refactor(launch): merge pre/post launch command steps (#6037)

This commit is contained in:
Alexandru Ionut Tripon authored and GitHub committed 2026-09-09 14:38:42 +00:00
commit b69f059c8f
12 files changed
+200 -272

No files matched your search

+2 -4
View File
@@ -147,10 +147,8 @@ set(LAUNCH_SOURCES
launch/steps/CheckJava.h
launch/steps/LookupServerAddress.cpp
launch/steps/LookupServerAddress.h
launch/steps/PostLaunchCommand.cpp
launch/steps/PostLaunchCommand.h
launch/steps/PreLaunchCommand.cpp
launch/steps/PreLaunchCommand.h
launch/steps/LaunchCommand.cpp
launch/steps/LaunchCommand.h
launch/steps/TextPrint.cpp
launch/steps/TextPrint.h
launch/steps/QuitAfterGameStop.cpp
+77 -9
View File
@@ -44,7 +44,7 @@
namespace Commandline {
// commandline splitter
QStringList splitArgs(QString args)
QStringList splitArgs(const QString& args)
{
QStringList argv;
QString current;
@@ -59,27 +59,95 @@ QStringList splitArgs(QString args)
escape = false;
// in "quotes"
} else if (!inquotes.isNull()) {
if (cchar == '\\')
if (cchar == '\\') {
escape = true;
else if (cchar == inquotes)
} else if (cchar == inquotes) {
inquotes = QChar::Null;
else
} else {
current += cchar;
}
// otherwise
} else {
if (cchar == ' ') {
if (cchar.isSpace()) {
if (!current.isEmpty()) {
argv << current;
current.clear();
}
} else if (cchar == '"' || cchar == '\'')
} else if (cchar == '"' || cchar == '\'') {
inquotes = cchar;
else
} else {
current += cchar;
}
}
}
if (!current.isEmpty())
if (!current.isEmpty()) {
argv << current;
}
return argv;
}
} // namespace Commandline
QString expandVariables(const QString& input, const QProcessEnvironment& dict)
{
QString result = input;
enum class State : std::uint8_t { Base, MaybeBrace, Variable, Brace } state = State::Base;
int startIdx = -1;
for (int i = 0; i < result.length();) {
const QChar c = result.at(i++);
switch (state) {
case State::Base:
if (c == '$') {
state = State::MaybeBrace;
}
break;
case State::MaybeBrace:
if (c == '{') {
state = State::Brace;
startIdx = i;
} else if (c.isLetterOrNumber() || c == '_') {
state = State::Variable;
startIdx = i - 1;
} else {
state = State::Base;
}
break;
case State::Brace:
if (c == '}') {
const auto res = dict.value(result.mid(startIdx, i - 1 - startIdx), "");
if (!res.isEmpty()) {
result.replace(startIdx - 2, i - startIdx + 2, res);
i = startIdx - 2 + res.length();
}
state = State::Base;
}
break;
case State::Variable:
if (!c.isLetterOrNumber() && c != '_') {
const auto res = dict.value(result.mid(startIdx, i - startIdx - 1), "");
if (!res.isEmpty()) {
result.replace(startIdx - 1, i - startIdx, res);
i = startIdx - 1 + res.length();
}
state = State::Base;
}
break;
}
}
if (state == State::Variable) {
if (const auto res = dict.value(result.mid(startIdx), ""); !res.isEmpty()) {
result.replace(startIdx - 1, result.length() - startIdx + 1, res);
}
}
return result;
}
QStringList process(const QString& cmd, const QProcessEnvironment& dict)
{
auto splited = splitArgs(cmd);
for (auto& arg : splited) {
arg = expandVariables(arg, dict);
}
return splited;
}
} // namespace Commandline
+19 -1
View File
@@ -17,6 +17,7 @@
#pragma once
#include <QProcessEnvironment>
#include <QString>
#include <QStringList>
@@ -32,5 +33,22 @@ namespace Commandline {
* @param args the argument string
* @return a QStringList containing all arguments
*/
QStringList splitArgs(QString args);
QStringList splitArgs(const QString& args);
/**
* @brief expand variables in a string like a shell would do
* @param input the input string
* @param dict the environment dictionary
* @return the expanded string
*/
QString expandVariables(const QString& input, const QProcessEnvironment& dict);
/**
* @brief process a commandline string into a QStringList of arguments, expanding variables
* @param cmd the commandline string
* @param dict the environment dictionary
* @return a QStringList containing all arguments
*/
QStringList process(const QString& cmd, const QProcessEnvironment& dict = {});
} // namespace Commandline
+5 -57
View File
@@ -43,6 +43,7 @@
#include <QDir>
#include <QStandardPaths>
#include <variant>
#include "Commandline.h"
#include "MessageLevel.h"
#include "tasks/Task.h"
@@ -215,7 +216,7 @@ shared_qobject_ptr<LogModel> LaunchTask::getLogModel()
return m_logModel;
}
bool LaunchTask::parseXmlLogs(QString const& line, MessageLevel level)
bool LaunchTask::parseXmlLogs(const QString& line, MessageLevel level)
{
LogParser* parser;
switch (static_cast<MessageLevel::Enum>(level)) {
@@ -241,7 +242,7 @@ bool LaunchTask::parseXmlLogs(QString const& line, MessageLevel level)
return true;
auto model = getLogModel();
for (auto const& item : items) {
for (const auto& item : items) {
if (std::holds_alternative<LogParser::LogEntry>(item)) {
auto entry = std::get<LogParser::LogEntry>(item);
auto msg = QString("[%1] [%2/%3] [%4]: %5")
@@ -301,60 +302,7 @@ void LaunchTask::emitFailed(QString reason)
Task::emitFailed(reason);
}
QString expandVariables(const QString& input, QProcessEnvironment dict)
QStringList LaunchTask::substituteVariables(QString& cmd, bool isLaunch) const
{
QString result = input;
enum { base, maybeBrace, variable, brace } state = base;
int startIdx = -1;
for (int i = 0; i < result.length();) {
QChar c = result.at(i++);
switch (state) {
case base:
if (c == '$')
state = maybeBrace;
break;
case maybeBrace:
if (c == '{') {
state = brace;
startIdx = i;
} else if (c.isLetterOrNumber() || c == '_') {
state = variable;
startIdx = i - 1;
} else {
state = base;
}
break;
case brace:
if (c == '}') {
const auto res = dict.value(result.mid(startIdx, i - 1 - startIdx), "");
if (!res.isEmpty()) {
result.replace(startIdx - 2, i - startIdx + 2, res);
i = startIdx - 2 + res.length();
}
state = base;
}
break;
case variable:
if (!c.isLetterOrNumber() && c != '_') {
const auto res = dict.value(result.mid(startIdx, i - startIdx - 1), "");
if (!res.isEmpty()) {
result.replace(startIdx - 1, i - startIdx, res);
i = startIdx - 1 + res.length();
}
state = base;
}
break;
}
}
if (state == variable) {
if (const auto res = dict.value(result.mid(startIdx), ""); !res.isEmpty())
result.replace(startIdx - 1, result.length() - startIdx + 1, res);
}
return result;
}
QString LaunchTask::substituteVariables(QString& cmd, bool isLaunch) const
{
return expandVariables(cmd, isLaunch ? m_instance->createLaunchEnvironment() : m_instance->createEnvironment());
return Commandline::process(cmd, isLaunch ? m_instance->createLaunchEnvironment() : m_instance->createEnvironment());
}
+2 -2
View File
@@ -87,7 +87,7 @@ class LaunchTask : public Task {
shared_qobject_ptr<LogModel> getLogModel();
public:
QString substituteVariables(QString& cmd, bool isLaunch = false) const;
QStringList substituteVariables(QString& cmd, bool isLaunch = false) const;
QString censorPrivateInfo(QString in);
protected: /* methods */
@@ -115,7 +115,7 @@ class LaunchTask : public Task {
void finalizeSteps(bool successful, const QString& error);
protected:
bool parseXmlLogs(QString const& line, MessageLevel level);
bool parseXmlLogs(const QString& line, MessageLevel level);
protected: /* data */
MinecraftInstance* m_instance;
@@ -33,31 +33,40 @@
* limitations under the License.
*/
#include "PostLaunchCommand.h"
#include "LaunchCommand.h"
#include <launch/LaunchTask.h>
PostLaunchCommand::PostLaunchCommand(LaunchTask* parent) : LaunchStep(parent)
#include <utility>
LaunchCommand::LaunchCommand(LaunchTask* parent, QString command, QString phaseName)
: LaunchStep(parent), m_command(std::move(command)), m_phaseName(std::move(phaseName))
{
auto instance = m_parent->instance();
m_command = instance->getPostExitCommand();
auto* instance = m_parent->instance();
m_process.setProcessEnvironment(instance->createEnvironment());
connect(&m_process, &LoggedProcess::log, this, &PostLaunchCommand::logLines);
connect(&m_process, &LoggedProcess::stateChanged, this, &PostLaunchCommand::on_state);
connect(&m_process, &LoggedProcess::log, this, &LaunchCommand::logLines);
connect(&m_process, &LoggedProcess::stateChanged, this, &LaunchCommand::onState);
}
void PostLaunchCommand::executeTask()
void LaunchCommand::executeTask()
{
auto cmd = m_parent->substituteVariables(m_command);
emit logLine(tr("Running Post-Launch command: %1").arg(cmd), MessageLevel::Launcher);
auto args = QProcess::splitCommand(cmd);
auto args = m_parent->substituteVariables(m_command);
emit logLine(tr("Running %1 command: %2").arg(m_phaseName, args.join(' ')), MessageLevel::Launcher);
if (args.isEmpty()) {
auto error = tr("%1 command is empty, skipping.").arg(m_phaseName);
emit logLine(error, MessageLevel::Fatal);
emitFailed(error);
return;
}
const QString program = args.takeFirst();
m_process.start(program, args);
}
void PostLaunchCommand::on_state(LoggedProcess::State state)
void LaunchCommand::onState(LoggedProcess::State state)
{
auto getError = [this]() { return tr("Post-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); };
auto getError = [this]() {
auto error = tr("%1 command failed with code %2.\n\n").arg(m_phaseName).arg(m_process.exitCode());
return error;
};
switch (state) {
case LoggedProcess::Aborted:
case LoggedProcess::Crashed:
@@ -73,7 +82,7 @@ void PostLaunchCommand::on_state(LoggedProcess::State state)
emit logLine(error, MessageLevel::Fatal);
emitFailed(error);
} else {
emit logLine(tr("Post-Launch command ran successfully.\n\n"), MessageLevel::Launcher);
emit logLine(tr("%1 command ran successfully.\n\n").arg(m_phaseName), MessageLevel::Launcher);
emitSucceeded();
}
}
@@ -82,12 +91,12 @@ void PostLaunchCommand::on_state(LoggedProcess::State state)
}
}
void PostLaunchCommand::setWorkingDirectory(const QString& wd)
void LaunchCommand::setWorkingDirectory(const QString& wd)
{
m_process.setWorkingDirectory(wd);
}
bool PostLaunchCommand::abort()
bool LaunchCommand::abort()
{
auto state = m_process.state();
if (state == LoggedProcess::Running || state == LoggedProcess::Starting) {
+58
View File
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* 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 <LoggedProcess.h>
#include <launch/LaunchStep.h>
class LaunchCommand : public LaunchStep {
Q_OBJECT
public:
LaunchCommand(LaunchTask* parent, QString command, QString phaseName = {});
~LaunchCommand() override = default;
void executeTask() override;
bool abort() override;
bool canAbort() const override { return true; }
void setWorkingDirectory(const QString& wd);
private slots:
void onState(LoggedProcess::State state);
private:
LoggedProcess m_process;
QString m_command;
QString m_phaseName;
};
-37
View File
@@ -1,37 +0,0 @@
/* Copyright 2013-2021 MultiMC Contributors
*
* 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 <LoggedProcess.h>
#include <launch/LaunchStep.h>
class PostLaunchCommand : public LaunchStep {
Q_OBJECT
public:
explicit PostLaunchCommand(LaunchTask* parent);
virtual ~PostLaunchCommand() {};
virtual void executeTask();
virtual bool abort();
virtual bool canAbort() const { return true; }
void setWorkingDirectory(const QString& wd);
private slots:
void on_state(LoggedProcess::State state);
private:
LoggedProcess m_process;
QString m_command;
};
@@ -1,96 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* 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 "PreLaunchCommand.h"
#include <launch/LaunchTask.h>
PreLaunchCommand::PreLaunchCommand(LaunchTask* parent) : LaunchStep(parent)
{
auto instance = m_parent->instance();
m_command = instance->getPreLaunchCommand();
m_process.setProcessEnvironment(instance->createEnvironment());
connect(&m_process, &LoggedProcess::log, this, &PreLaunchCommand::logLines);
connect(&m_process, &LoggedProcess::stateChanged, this, &PreLaunchCommand::on_state);
}
void PreLaunchCommand::executeTask()
{
auto cmd = m_parent->substituteVariables(m_command);
emit logLine(tr("Running Pre-Launch command: %1").arg(cmd), MessageLevel::Launcher);
auto args = QProcess::splitCommand(cmd);
const QString program = args.takeFirst();
m_process.start(program, args);
}
void PreLaunchCommand::on_state(LoggedProcess::State state)
{
auto getError = [this]() { return tr("Pre-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); };
switch (state) {
case LoggedProcess::Aborted:
case LoggedProcess::Crashed:
case LoggedProcess::FailedToStart: {
auto error = getError();
emit logLine(error, MessageLevel::Fatal);
emitFailed(error);
return;
}
case LoggedProcess::Finished: {
if (m_process.exitCode() != 0) {
auto error = getError();
emit logLine(error, MessageLevel::Fatal);
emitFailed(error);
} else {
emit logLine(tr("Pre-Launch command ran successfully.\n\n"), MessageLevel::Launcher);
emitSucceeded();
}
}
default:
break;
}
}
void PreLaunchCommand::setWorkingDirectory(const QString& wd)
{
m_process.setWorkingDirectory(wd);
}
bool PreLaunchCommand::abort()
{
auto state = m_process.state();
if (state == LoggedProcess::Running || state == LoggedProcess::Starting) {
m_process.kill();
}
return true;
}
-37
View File
@@ -1,37 +0,0 @@
/* Copyright 2013-2021 MultiMC Contributors
*
* 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 "LoggedProcess.h"
#include "launch/LaunchStep.h"
class PreLaunchCommand : public LaunchStep {
Q_OBJECT
public:
explicit PreLaunchCommand(LaunchTask* parent);
virtual ~PreLaunchCommand() {};
virtual void executeTask();
virtual bool abort();
virtual bool canAbort() const { return true; }
void setWorkingDirectory(const QString& wd);
private slots:
void on_state(LoggedProcess::State state);
private:
LoggedProcess m_process;
QString m_command;
};
+3 -4
View File
@@ -51,8 +51,7 @@
#include "launch/TaskStepWrapper.h"
#include "launch/steps/CheckJava.h"
#include "launch/steps/LookupServerAddress.h"
#include "launch/steps/PostLaunchCommand.h"
#include "launch/steps/PreLaunchCommand.h"
#include "launch/steps/LaunchCommand.h"
#include "launch/steps/QuitAfterGameStop.h"
#include "launch/steps/TextPrint.h"
@@ -1191,7 +1190,7 @@ LaunchTask* MinecraftInstance::createLaunchTask(AuthSessionPtr session, Minecraf
// run pre-launch command if that's needed
if (getPreLaunchCommand().size()) {
auto step = makeShared<PreLaunchCommand>(pptr);
auto step = makeShared<LaunchCommand>(pptr, getPreLaunchCommand(), tr("Pre-Launch"));
step->setWorkingDirectory(gameRoot());
process->appendStep(step);
}
@@ -1247,7 +1246,7 @@ LaunchTask* MinecraftInstance::createLaunchTask(AuthSessionPtr session, Minecraf
// run post-exit command if that's needed
if (getPostExitCommand().size()) {
auto step = makeShared<PostLaunchCommand>(pptr);
auto step = makeShared<LaunchCommand>(pptr, getPostExitCommand(), tr("Post-Launch"));
step->setWorkingDirectory(gameRoot());
process->appendStep(step);
}
@@ -39,7 +39,6 @@
#include <QStandardPaths>
#include "Application.h"
#include "Commandline.h"
#include "FileSystem.h"
#include "launch/LaunchTask.h"
#include "minecraft/MinecraftInstance.h"
@@ -79,7 +78,7 @@ void LauncherPartLaunch::executeTask()
return;
}
auto instance = m_parent->instance();
auto* instance = m_parent->instance();
QString legacyJarPath;
if (instance->getLauncher() == "legacy" || instance->shouldApplyOnlineFixes()) {
@@ -107,8 +106,9 @@ void LauncherPartLaunch::executeTask()
auto classPath = instance->getClassPath();
classPath.prepend(jarPath);
if (!legacyJarPath.isEmpty())
if (!legacyJarPath.isEmpty()) {
classPath.prepend(legacyJarPath);
}
auto natPath = instance->getNativePath();
#ifdef Q_OS_WIN
@@ -132,8 +132,7 @@ void LauncherPartLaunch::executeTask()
QString wrapperCommandStr = instance->getWrapperCommand().trimmed();
if (!wrapperCommandStr.isEmpty()) {
wrapperCommandStr = m_parent->substituteVariables(wrapperCommandStr);
auto wrapperArgs = Commandline::splitArgs(wrapperCommandStr);
auto wrapperArgs = m_parent->substituteVariables(wrapperCommandStr);
auto wrapperCommand = wrapperArgs.takeFirst();
auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand);
if (realWrapperCommand.isEmpty()) {
@@ -152,8 +151,8 @@ void LauncherPartLaunch::executeTask()
#ifdef Q_OS_LINUX
if (instance->settings()->get("EnableFeralGamemode").toBool() && APPLICATION->capabilities() & Application::SupportsGameMode) {
auto pid = m_process.processId();
if (pid) {
gamemode_request_start_for(pid);
if (pid != 0) {
gamemode_request_start_for(static_cast<pid_t>(pid));
}
}
#endif
@@ -177,9 +176,10 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state)
return;
}
case LoggedProcess::Finished: {
auto instance = m_parent->instance();
if (instance->settings()->get("CloseAfterLaunch").toBool())
auto* instance = m_parent->instance();
if (instance->settings()->get("CloseAfterLaunch").toBool()) {
APPLICATION->showMainWindow();
}
m_parent->setPid(-1);
m_parent->instance()->setMinecraftRunning(false);