diff options
Diffstat (limited to 'launcher')
50 files changed, 2910 insertions, 413 deletions
diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 8680361c..a8fce879 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -40,6 +40,8 @@ #include <QDir> #include <QDebug> #include <QRegularExpression> +#include <QJsonDocument> +#include <QJsonObject> #include "settings/INISettingsObject.h" #include "settings/Setting.h" @@ -64,6 +66,8 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerSetting("totalTimePlayed", 0); m_settings->registerSetting("lastTimePlayed", 0); + m_settings->registerSetting("linkedInstances", "[]"); + // Game time override auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false); m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"), gameTimeOverride); @@ -182,6 +186,38 @@ bool BaseInstance::shouldStopOnConsoleOverflow() const return m_settings->get("ConsoleOverflowStop").toBool(); } +QStringList BaseInstance::getLinkedInstances() const +{ + return m_settings->get("linkedInstances").toStringList(); +} + +void BaseInstance::setLinkedInstances(const QStringList& list) +{ + auto linkedInstances = m_settings->get("linkedInstances").toStringList(); + m_settings->set("linkedInstances", list); +} + +void BaseInstance::addLinkedInstanceId(const QString& id) +{ + auto linkedInstances = m_settings->get("linkedInstances").toStringList(); + linkedInstances.append(id); + setLinkedInstances(linkedInstances); +} + +bool BaseInstance::removeLinkedInstanceId(const QString& id) +{ + auto linkedInstances = m_settings->get("linkedInstances").toStringList(); + int numRemoved = linkedInstances.removeAll(id); + setLinkedInstances(linkedInstances); + return numRemoved > 0; +} + +bool BaseInstance::isLinkedToInstanceId(const QString& id) const +{ + auto linkedInstances = m_settings->get("linkedInstances").toStringList(); + return linkedInstances.contains(id); +} + void BaseInstance::iconUpdated(QString key) { if(iconKey() == key) diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index a2a4f824..83a8064f 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -282,6 +282,12 @@ public: int getConsoleMaxLines() const; bool shouldStopOnConsoleOverflow() const; + QStringList getLinkedInstances() const; + void setLinkedInstances(const QStringList& list); + void addLinkedInstanceId(const QString& id); + bool removeLinkedInstanceId(const QString& id); + bool isLinkedToInstanceId(const QString& id) const; + protected: void changeStatus(Status newStatus); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index a3ef20e8..273b5449 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -26,6 +26,7 @@ set(CORE_SOURCES MMCZip.cpp StringUtils.h StringUtils.cpp + QVariantUtils.h RuntimeContext.h # Basic instance manipulation tasks (derived from InstanceTask) @@ -554,6 +555,18 @@ set(ATLAUNCHER_SOURCES modplatform/atlauncher/ATLShareCode.h ) +set(LINKEXE_SOURCES + filelink/FileLink.h + filelink/FileLink.cpp + FileSystem.h + FileSystem.cpp + Exception.h + StringUtils.h + StringUtils.cpp + DesktopServices.h + DesktopServices.cpp +) + ######## Logging categories ######## ecm_qt_declare_logging_category(CORE_SOURCES @@ -1145,6 +1158,41 @@ install(TARGETS ${Launcher_Name} FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) +if(WIN32) + add_library(filelink_logic STATIC ${LINKEXE_SOURCES}) + target_include_directories(filelink_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(filelink_logic + systeminfo + BuildConfig + ghcFilesystem::ghc_filesystem + Qt${QT_VERSION_MAJOR}::Widgets + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Network + # Qt${QT_VERSION_MAJOR}::Concurrent + ${Launcher_QT_LIBS} + ) + + add_executable("${Launcher_Name}_filelink" WIN32 filelink/main.cpp) + + target_sources("${Launcher_Name}_filelink" PRIVATE filelink/filelink.exe.manifest) + + target_link_libraries("${Launcher_Name}_filelink" filelink_logic) + + if(DEFINED Launcher_APP_BINARY_NAME) + set_target_properties("${Launcher_Name}_filelink" PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}_filelink") + endif() + if(DEFINED Launcher_BINARY_RPATH) + SET_TARGET_PROPERTIES("${Launcher_Name}_filelink" PROPERTIES INSTALL_RPATH "${Launcher_BINARY_RPATH}") + endif() + + install(TARGETS "${Launcher_Name}_filelink" + BUNDLE DESTINATION "." COMPONENT Runtime + LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime + RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime + FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime + ) +endif() + if (UNIX AND APPLE) # Add Sparkle updater # It has to be copied here instead of just allowing fixup_bundle to install it, otherwise essential parts of diff --git a/launcher/DesktopServices.cpp b/launcher/DesktopServices.cpp index 302eaf96..2984a1b4 100644 --- a/launcher/DesktopServices.cpp +++ b/launcher/DesktopServices.cpp @@ -37,7 +37,6 @@ #include <QDesktopServices> #include <QProcess> #include <QDebug> -#include "Application.h" /** * This shouldn't exist, but until QTBUG-9328 and other unreported bugs are fixed, it needs to be a thing. diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index aee5245d..d98526df 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -3,6 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me> + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * 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 @@ -36,6 +37,8 @@ #include "FileSystem.h" +#include "BuildConfig.h" + #include <QDebug> #include <QDir> #include <QDirIterator> @@ -43,13 +46,17 @@ #include <QFileInfo> #include <QSaveFile> #include <QStandardPaths> +#include <QStorageInfo> #include <QTextStream> #include <QUrl> +#include <QtNetwork> +#include <system_error> #include "DesktopServices.h" #include "StringUtils.h" #if defined Q_OS_WIN32 +#define NOMINMAX #define WIN32_LEAN_AND_MEAN #include <objbase.h> #include <objidl.h> @@ -61,6 +68,10 @@ #include <windows.h> #include <winnls.h> #include <string> +// for ShellExecute +#include <Shellapi.h> +#include <objbase.h> +#include <shlobj.h> #else #include <utime.h> #endif @@ -68,22 +79,96 @@ // Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header #ifdef __APPLE__ -#include <Availability.h> // for deployment target to support pre-catalina targets without std::fs -#endif // __APPLE__ +#include <Availability.h> // for deployment target to support pre-catalina targets without std::fs +#endif // __APPLE__ #if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include) #if __has_include(<filesystem>) && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500) #define GHC_USE_STD_FS #include <filesystem> namespace fs = std::filesystem; -#endif // MacOS min version check -#endif // Other OSes version check +#endif // MacOS min version check +#endif // Other OSes version check #ifndef GHC_USE_STD_FS #include <ghc/filesystem.hpp> namespace fs = ghc::filesystem; #endif +// clone +#if defined(Q_OS_LINUX) +#include <errno.h> +#include <fcntl.h> /* Definition of FICLONE* constants */ +#include <linux/fs.h> +#include <sys/ioctl.h> +#include <unistd.h> +#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) +#include <sys/attr.h> +#include <sys/clonefile.h> +#elif defined(Q_OS_WIN) +// winbtrfs clone vs rundll32 shellbtrfs.dll,ReflinkCopy +#include <fileapi.h> +#include <stdio.h> +#include <tchar.h> +#include <windows.h> +// refs +#include <winioctl.h> +#if defined(__MINGW32__) +#include <crtdbg.h> +#endif +#endif + +#if defined(Q_OS_WIN) + +#if defined(__MINGW32__) + +typedef struct _DUPLICATE_EXTENTS_DATA { + HANDLE FileHandle; + LARGE_INTEGER SourceFileOffset; + LARGE_INTEGER TargetFileOffset; + LARGE_INTEGER ByteCount; +} DUPLICATE_EXTENTS_DATA, *PDUPLICATE_EXTENTS_DATA; + +typedef struct _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER { + WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32 + WORD Reserved; // Must be 0 + DWORD Flags; // FSCTL_INTEGRITY_FLAG_xxx + DWORD ChecksumChunkSizeInBytes; + DWORD ClusterSizeInBytes; +} FSCTL_GET_INTEGRITY_INFORMATION_BUFFER, *PFSCTL_GET_INTEGRITY_INFORMATION_BUFFER; + +typedef struct _FSCTL_SET_INTEGRITY_INFORMATION_BUFFER { + WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32 + WORD Reserved; // Must be 0 + DWORD Flags; // FSCTL_INTEGRITY_FLAG_xxx +} FSCTL_SET_INTEGRITY_INFORMATION_BUFFER, *PFSCTL_SET_INTEGRITY_INFORMATION_BUFFER; + +#endif + +#ifndef FSCTL_DUPLICATE_EXTENTS_TO_FILE +#define FSCTL_DUPLICATE_EXTENTS_TO_FILE CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 209, METHOD_BUFFERED, FILE_WRITE_DATA) +#endif + +#ifndef FSCTL_GET_INTEGRITY_INFORMATION +#define FSCTL_GET_INTEGRITY_INFORMATION \ + CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 159, METHOD_BUFFERED, FILE_ANY_ACCESS) // FSCTL_GET_INTEGRITY_INFORMATION_BUFFER +#endif + +#ifndef FSCTL_SET_INTEGRITY_INFORMATION +#define FSCTL_SET_INTEGRITY_INFORMATION \ + CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 160, METHOD_BUFFERED, FILE_READ_DATA | FILE_WRITE_DATA) // FSCTL_SET_INTEGRITY_INFORMATION_BUFFER +#endif + +#ifndef ERROR_NOT_CAPABLE +#define ERROR_NOT_CAPABLE 775L +#endif + +#ifndef ERROR_BLOCK_TOO_MANY_REFERENCES +#define ERROR_BLOCK_TOO_MANY_REFERENCES 347L +#endif + +#endif + namespace FS { void ensureExists(const QDir& dir) @@ -152,9 +237,11 @@ bool ensureFolderPathExists(QString foldernamepath) return success; } -/// @brief Copies a directory and it's contents from src to dest -/// @param offset subdirectory form src to copy to dest -/// @return if there was an error during the filecopy +/** + * @brief Copies a directory and it's contents from src to dest + * @param offset subdirectory form src to copy to dest + * @return if there was an error during the filecopy + */ bool copy::operator()(const QString& offset, bool dryRun) { using copy_opts = fs::copy_options; @@ -215,6 +302,271 @@ bool copy::operator()(const QString& offset, bool dryRun) return err.value() == 0; } +/// qDebug print support for the LinkPair struct +QDebug operator<<(QDebug debug, const LinkPair& lp) +{ + QDebugStateSaver saver(debug); + + debug.nospace() << "LinkPair{ src: " << lp.src << " , dst: " << lp.dst << " }"; + return debug; +} + +bool create_link::operator()(const QString& offset, bool dryRun) +{ + m_linked = 0; // reset counter + m_path_results.clear(); + m_links_to_make.clear(); + + m_path_results.clear(); + + make_link_list(offset); + + if (!dryRun) + return make_links(); + + return true; +} + +/** + * @brief Make a list of all the links to make + * @param offset subdirectory of src to link to dest + */ +void create_link::make_link_list(const QString& offset) +{ + for (auto pair : m_path_pairs) { + const QString& srcPath = pair.src; + const QString& dstPath = pair.dst; + + auto src = PathCombine(QDir(srcPath).absolutePath(), offset); + auto dst = PathCombine(QDir(dstPath).absolutePath(), offset); + + // you can't hard link a directory so make sure if we deal with a directory we do so recursively + if (m_useHardLinks) + m_recursive = true; + + // Function that'll do the actual linking + auto link_file = [&](QString src_path, QString relative_dst_path) { + if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist)) { + qDebug() << "path" << relative_dst_path << "in black list or not in whitelist"; + return; + } + + auto dst_path = PathCombine(dst, relative_dst_path); + LinkPair link = { src_path, dst_path }; + m_links_to_make.append(link); + }; + + if ((!m_recursive) || !fs::is_directory(StringUtils::toStdString(src))) { + if (m_debug) + qDebug() << "linking single file or dir:" << src << "to" << dst; + link_file(src, ""); + } else { + if (m_debug) + qDebug() << "linking recursively:" << src << "to" << dst << ", max_depth:" << m_max_depth; + QDir src_dir(src); + QDirIterator source_it(src, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::Subdirectories); + + QStringList linkedPaths; + + while (source_it.hasNext()) { + auto src_path = source_it.next(); + auto relative_path = src_dir.relativeFilePath(src_path); + + if (m_max_depth >= 0 && pathDepth(relative_path) > m_max_depth){ + relative_path = pathTruncate(relative_path, m_max_depth); + src_path = src_dir.filePath(relative_path); + if (linkedPaths.contains(src_path)) { + continue; + } + } + + linkedPaths.append(src_path); + + link_file(src_path, relative_path); + } + } + } +} + +bool create_link::make_links() +{ + for (auto link : m_links_to_make) { + QString src_path = link.src; + QString dst_path = link.dst; + auto src_path_std = StringUtils::toStdString(link.src); + auto dst_path_std = StringUtils::toStdString(link.dst); + + ensureFilePathExists(dst_path); + if (m_useHardLinks) { + if (m_debug) + qDebug() << "making hard link:" << src_path << "to" << dst_path; + fs::create_hard_link(src_path_std, dst_path_std, m_os_err); + } else if (fs::is_directory(src_path_std)) { + if (m_debug) + qDebug() << "making directory_symlink:" << src_path << "to" << dst_path; + fs::create_directory_symlink(src_path_std, dst_path_std, m_os_err); + } else { + if (m_debug) + qDebug() << "making symlink:" << src_path << "to" << dst_path; + fs::create_symlink(src_path_std, dst_path_std, m_os_err); + } + + if (m_os_err) { + qWarning() << "Failed to link files:" << QString::fromStdString(m_os_err.message()); + qDebug() << "Source file:" << src_path; + qDebug() << "Destination file:" << dst_path; + qDebug() << "Error category:" << m_os_err.category().name(); + qDebug() << "Error code:" << m_os_err.value(); + emit linkFailed(src_path, dst_path, QString::fromStdString(m_os_err.message()), m_os_err.value()); + } else { + m_linked++; + emit fileLinked(src_path, dst_path); + } + if (m_os_err) + return false; + } + return true; +} + +void create_link::runPrivileged(const QString& offset) +{ + m_linked = 0; // reset counter + m_path_results.clear(); + m_links_to_make.clear(); + + bool gotResults = false; + + make_link_list(offset); + + QString serverName = BuildConfig.LAUNCHER_APP_BINARY_NAME + "_filelink_server" + StringUtils::getRandomAlphaNumeric(); + + connect(&m_linkServer, &QLocalServer::newConnection, this, [&]() { + qDebug() << "Client connected, sending out pairs"; + // construct block of data to send + QByteArray block; + QDataStream out(&block, QIODevice::WriteOnly); + + qint32 blocksize = quint32(sizeof(quint32)); + for (auto link : m_links_to_make) { + blocksize += quint32(link.src.size()); + blocksize += quint32(link.dst.size()); + } + qDebug() << "About to write block of size:" << blocksize; + out << blocksize; + + out << quint32(m_links_to_make.length()); + for (auto link : m_links_to_make) { + out << link.src; + out << link.dst; + } + + QLocalSocket* clientConnection = m_linkServer.nextPendingConnection(); + connect(clientConnection, &QLocalSocket::disconnected, clientConnection, &QLocalSocket::deleteLater); + + connect(clientConnection, &QLocalSocket::readyRead, this, [&, clientConnection]() { + QDataStream in; + quint32 blockSize = 0; + in.setDevice(clientConnection); + + qDebug() << "Reading path results from client"; + qDebug() << "bytes available" << clientConnection->bytesAvailable(); + + // Relies on the fact that QDataStream serializes a quint32 into + // sizeof(quint32) bytes + if (clientConnection->bytesAvailable() < (int)sizeof(quint32)) + return; + qDebug() << "reading block size"; + in >> blockSize; + + qDebug() << "blocksize is" << blockSize; + qDebug() << "bytes available" << clientConnection->bytesAvailable(); + if (clientConnection->bytesAvailable() < blockSize || in.atEnd()) + return; + + quint32 numResults; + in >> numResults; + qDebug() << "numResults" << numResults; + + for (quint32 i = 0; i < numResults; i++) { + FS::LinkResult result; + in >> result.src; + in >> result.dst; + in >> result.err_msg; + qint32 err_value; + in >> err_value; + result.err_value = err_value; + if (result.err_value) { + qDebug() << "privileged link fail" << result.src << "to" << result.dst << "code" << result.err_value << result.err_msg; + emit linkFailed(result.src, result.dst, result.err_msg, result.err_value); + } else { + qDebug() << "privileged link success" << result.src << "to" << result.dst; + m_linked++; + emit fileLinked(result.src, result.dst); + } + m_path_results.append(result); + } + gotResults = true; + qDebug() << "results received, closing connection"; + clientConnection->close(); + }); + + qint64 byteswritten = clientConnection->write(block); + bool bytesflushed = clientConnection->flush(); + qDebug() << "block flushed" << byteswritten << bytesflushed; + }); + + qDebug() << "Listening on pipe" << serverName; + if (!m_linkServer.listen(serverName)) { + qDebug() << "Unable to start local pipe server on" << serverName << ":" << m_linkServer.errorString(); + return; + } + + ExternalLinkFileProcess* linkFileProcess = new ExternalLinkFileProcess(serverName, m_useHardLinks, this); + connect(linkFileProcess, &ExternalLinkFileProcess::processExited, this, [&]() { emit finishedPrivileged(gotResults); }); + connect(linkFileProcess, &ExternalLinkFileProcess::finished, linkFileProcess, &QObject::deleteLater); + + linkFileProcess->start(); +} + +void ExternalLinkFileProcess::runLinkFile() +{ + QString fileLinkExe = + PathCombine(QCoreApplication::instance()->applicationDirPath(), BuildConfig.LAUNCHER_APP_BINARY_NAME + "_filelink"); + QString params = "-s " + m_server; + + params += " -H " + QVariant(m_useHardLinks).toString(); + +#if defined Q_OS_WIN32 + SHELLEXECUTEINFO ShExecInfo; + + fileLinkExe = fileLinkExe + ".exe"; + + qDebug() << "Running: runas" << fileLinkExe << params; + + LPCWSTR programNameWin = (const wchar_t*)fileLinkExe.utf16(); + LPCWSTR paramsWin = (const wchar_t*)params.utf16(); + + // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-shellexecuteinfoa + ShExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); + ShExecInfo.fMask = SEE_MASK_NOCLOSEPROCESS; + ShExecInfo.hwnd = NULL; // Optional. A handle to the owner window, used to display and position any UI that the system might produce + // while executing this function. + ShExecInfo.lpVerb = L"runas"; // elevate to admin, show UAC + ShExecInfo.lpFile = programNameWin; + ShExecInfo.lpParameters = paramsWin; + ShExecInfo.lpDirectory = NULL; + ShExecInfo.nShow = SW_HIDE; + ShExecInfo.hInstApp = NULL; + + ShellExecuteEx(&ShExecInfo); + + WaitForSingleObject(ShExecInfo.hProcess, INFINITE); + CloseHandle(ShExecInfo.hProcess); +#endif + + qDebug() << "Process exited"; +} + bool move(const QString& source, const QString& dest) { std::error_code err; @@ -244,7 +596,7 @@ bool deletePath(QString path) return err.value() == 0; } -bool trash(QString path, QString *pathInTrash) +bool trash(QString path, QString* pathInTrash) { #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) return false; @@ -279,11 +631,60 @@ QString PathCombine(const QString& path1, const QString& path2, const QString& p return PathCombine(PathCombine(path1, path2, path3), path4); } -QString AbsolutePath(QString path) +QString AbsolutePath(const QString& path) { return QFileInfo(path).absolutePath(); } +int pathDepth(const QString& path) +{ + if (path.isEmpty()) + return 0; + + QFileInfo info(path); + +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) + auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), QString::SkipEmptyParts); +#else + auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), Qt::SkipEmptyParts); +#endif + + int numParts = parts.length(); + numParts -= parts.count("."); + numParts -= parts.count("..") * 2; + + return numParts; +} + +QString pathTruncate(const QString& path, int depth) +{ + if (path.isEmpty() || (depth < 0)) + return ""; + + QString trunc = QFileInfo(path).path(); + + if (pathDepth(trunc) > depth ) { + return pa |
