mirror of
https://github.com/RetroDECK/Duckstation.git
synced 2024-11-26 15:45:42 +00:00
Qt: Update elevator for non-writable paths
i.e. support installation in Program Files.
This commit is contained in:
parent
058fe13c52
commit
3bd9cbdfec
|
@ -1,4 +1,4 @@
|
||||||
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
|
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
|
||||||
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
||||||
|
|
||||||
#include "autoupdaterdialog.h"
|
#include "autoupdaterdialog.h"
|
||||||
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
#include "util/http_downloader.h"
|
#include "util/http_downloader.h"
|
||||||
|
|
||||||
|
#include "common/error.h"
|
||||||
#include "common/file_system.h"
|
#include "common/file_system.h"
|
||||||
#include "common/log.h"
|
#include "common/log.h"
|
||||||
#include "common/minizip_helpers.h"
|
#include "common/minizip_helpers.h"
|
||||||
|
@ -32,7 +33,10 @@
|
||||||
// Interval at which HTTP requests are polled.
|
// Interval at which HTTP requests are polled.
|
||||||
static constexpr u32 HTTP_POLL_INTERVAL = 10;
|
static constexpr u32 HTTP_POLL_INTERVAL = 10;
|
||||||
|
|
||||||
#ifdef __APPLE__
|
#if defined(_WIN32)
|
||||||
|
#include "common/windows_headers.h"
|
||||||
|
#include <shellapi.h>
|
||||||
|
#elif defined(__APPLE__)
|
||||||
#include "common/cocoa_tools.h"
|
#include "common/cocoa_tools.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -482,67 +486,71 @@ void AutoUpdaterDialog::remindMeLaterClicked()
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
|
||||||
bool AutoUpdaterDialog::processUpdate(const std::vector<u8>& update_data)
|
static constexpr char UPDATER_EXECUTABLE[] = "updater.exe";
|
||||||
|
static constexpr char UPDATER_ARCHIVE_NAME[] = "update.zip";
|
||||||
|
|
||||||
|
bool AutoUpdaterDialog::doesUpdaterNeedElevation(const std::string& application_dir) const
|
||||||
{
|
{
|
||||||
const QString update_directory = QCoreApplication::applicationDirPath();
|
// Try to create a dummy text file in the PCSX2 updater directory. If it fails, we probably won't have write
|
||||||
const QString update_zip_path = update_directory + QStringLiteral("\\update.zip");
|
// permission.
|
||||||
const QString updater_path = update_directory + QStringLiteral("\\updater.exe");
|
const std::string dummy_path = Path::Combine(application_dir, "update.txt");
|
||||||
|
auto fp = FileSystem::OpenManagedCFile(dummy_path.c_str(), "wb");
|
||||||
Q_ASSERT(!update_zip_path.isEmpty() && !updater_path.isEmpty() && !update_directory.isEmpty());
|
if (!fp)
|
||||||
if ((QFile::exists(update_zip_path) && !QFile::remove(update_zip_path)) ||
|
|
||||||
(QFile::exists(updater_path) && !QFile::remove(updater_path)))
|
|
||||||
{
|
|
||||||
reportError("Removing existing update zip/updater failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
QFile update_zip_file(update_zip_path);
|
|
||||||
if (!update_zip_file.open(QIODevice::WriteOnly) ||
|
|
||||||
update_zip_file.write(reinterpret_cast<const char*>(update_data.data()),
|
|
||||||
static_cast<qint64>(update_data.size())) != static_cast<qint64>(update_data.size()))
|
|
||||||
{
|
|
||||||
reportError("Writing update zip to '%s' failed", update_zip_path.toUtf8().constData());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
update_zip_file.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!extractUpdater(update_zip_path, updater_path))
|
|
||||||
{
|
|
||||||
reportError("Extracting updater failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!doUpdate(update_zip_path, updater_path, update_directory))
|
|
||||||
{
|
|
||||||
reportError("Launching updater failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
fp.reset();
|
||||||
|
FileSystem::DeleteFile(dummy_path.c_str());
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AutoUpdaterDialog::extractUpdater(const QString& zip_path, const QString& destination_path)
|
bool AutoUpdaterDialog::processUpdate(const std::vector<u8>& update_data)
|
||||||
{
|
{
|
||||||
unzFile zf = MinizipHelpers::OpenUnzFile(zip_path.toUtf8().constData());
|
const std::string& application_dir = EmuFolders::AppRoot;
|
||||||
|
const std::string update_zip_path = Path::Combine(EmuFolders::DataRoot, UPDATER_ARCHIVE_NAME);
|
||||||
|
const std::string updater_path = Path::Combine(EmuFolders::DataRoot, UPDATER_EXECUTABLE);
|
||||||
|
|
||||||
|
if ((FileSystem::FileExists(update_zip_path.c_str()) && !FileSystem::DeleteFile(update_zip_path.c_str())))
|
||||||
|
{
|
||||||
|
reportError("Removing existing update zip failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FileSystem::WriteBinaryFile(update_zip_path.c_str(), update_data.data(), update_data.size()))
|
||||||
|
{
|
||||||
|
reportError("Writing update zip to '%s' failed", update_zip_path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Error updater_extract_error;
|
||||||
|
if (!extractUpdater(update_zip_path.c_str(), updater_path.c_str(), &updater_extract_error))
|
||||||
|
{
|
||||||
|
reportError("Extracting updater failed: %s", updater_extract_error.GetDescription().c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return doUpdate(application_dir, update_zip_path, updater_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AutoUpdaterDialog::extractUpdater(const std::string& zip_path, const std::string& destination_path, Error* error)
|
||||||
|
{
|
||||||
|
unzFile zf = MinizipHelpers::OpenUnzFile(zip_path.c_str());
|
||||||
if (!zf)
|
if (!zf)
|
||||||
{
|
{
|
||||||
reportError("Failed to open update zip");
|
reportError("Failed to open update zip");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unzLocateFile(zf, "updater.exe", 0) != UNZ_OK || unzOpenCurrentFile(zf) != UNZ_OK)
|
if (unzLocateFile(zf, UPDATER_EXECUTABLE, 0) != UNZ_OK || unzOpenCurrentFile(zf) != UNZ_OK)
|
||||||
{
|
{
|
||||||
reportError("Failed to locate updater.exe");
|
Error::SetString(error, "Failed to locate updater.exe");
|
||||||
unzClose(zf);
|
unzClose(zf);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QFile updater_exe(destination_path);
|
auto fp = FileSystem::OpenManagedCFile(destination_path.c_str(), "wb", error);
|
||||||
if (!updater_exe.open(QIODevice::WriteOnly))
|
if (!fp)
|
||||||
{
|
{
|
||||||
reportError("Failed to open updater.exe for writing");
|
Error::SetString(error, "Failed to open updater.exe for writing");
|
||||||
unzClose(zf);
|
unzClose(zf);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -554,10 +562,10 @@ bool AutoUpdaterDialog::extractUpdater(const QString& zip_path, const QString& d
|
||||||
int size = unzReadCurrentFile(zf, chunk, CHUNK_SIZE);
|
int size = unzReadCurrentFile(zf, chunk, CHUNK_SIZE);
|
||||||
if (size < 0)
|
if (size < 0)
|
||||||
{
|
{
|
||||||
reportError("Failed to decompress updater exe");
|
Error::SetString(error, "Failed to decompress updater exe");
|
||||||
unzClose(zf);
|
unzClose(zf);
|
||||||
updater_exe.close();
|
fp.reset();
|
||||||
updater_exe.remove();
|
FileSystem::DeleteFile(destination_path.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else if (size == 0)
|
else if (size == 0)
|
||||||
|
@ -565,44 +573,48 @@ bool AutoUpdaterDialog::extractUpdater(const QString& zip_path, const QString& d
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updater_exe.write(chunk, size) != size)
|
if (std::fwrite(chunk, size, 1, fp.get()) != 1)
|
||||||
{
|
{
|
||||||
reportError("Failed to write updater exe");
|
Error::SetString(error, "Failed to write updater exe");
|
||||||
unzClose(zf);
|
unzClose(zf);
|
||||||
updater_exe.close();
|
fp.reset();
|
||||||
updater_exe.remove();
|
FileSystem::DeleteFile(destination_path.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unzClose(zf);
|
unzClose(zf);
|
||||||
updater_exe.close();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AutoUpdaterDialog::doUpdate(const QString& zip_path, const QString& updater_path, const QString& destination_path)
|
bool AutoUpdaterDialog::doUpdate(const std::string& application_dir, const std::string& zip_path,
|
||||||
|
const std::string& updater_path)
|
||||||
{
|
{
|
||||||
const QString program_path = QCoreApplication::applicationFilePath();
|
const std::string program_path = QDir::toNativeSeparators(QCoreApplication::applicationFilePath()).toStdString();
|
||||||
if (program_path.isEmpty())
|
if (program_path.empty())
|
||||||
{
|
{
|
||||||
reportError("Failed to get current application path");
|
reportError("Failed to get current application path");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList arguments;
|
const std::wstring wupdater_path = StringUtil::UTF8StringToWideString(updater_path);
|
||||||
arguments << QString::number(QCoreApplication::applicationPid());
|
const std::wstring wapplication_dir = StringUtil::UTF8StringToWideString(application_dir);
|
||||||
arguments << destination_path;
|
const std::wstring arguments = StringUtil::UTF8StringToWideString(fmt::format(
|
||||||
arguments << zip_path;
|
"{} \"{}\" \"{}\" \"{}\"", QCoreApplication::applicationPid(), application_dir, zip_path, program_path));
|
||||||
arguments << program_path;
|
|
||||||
|
|
||||||
// this will leak, but not sure how else to handle it...
|
const bool needs_elevation = doesUpdaterNeedElevation(application_dir);
|
||||||
QProcess* updater_process = new QProcess();
|
|
||||||
updater_process->setProgram(updater_path);
|
SHELLEXECUTEINFOW sei = {};
|
||||||
updater_process->setArguments(arguments);
|
sei.cbSize = sizeof(sei);
|
||||||
updater_process->start(QIODevice::NotOpen);
|
sei.lpVerb = needs_elevation ? L"runas" : nullptr; // needed to trigger elevation
|
||||||
if (!updater_process->waitForStarted())
|
sei.lpFile = wupdater_path.c_str();
|
||||||
|
sei.lpParameters = arguments.c_str();
|
||||||
|
sei.lpDirectory = wapplication_dir.c_str();
|
||||||
|
sei.nShow = SW_SHOWNORMAL;
|
||||||
|
if (!ShellExecuteExW(&sei))
|
||||||
{
|
{
|
||||||
reportError("Failed to start updater");
|
reportError("Failed to start %s: %s", needs_elevation ? "elevated updater" : "updater",
|
||||||
|
Error::CreateWin32(GetLastError()).GetDescription().c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -611,6 +623,19 @@ bool AutoUpdaterDialog::doUpdate(const QString& zip_path, const QString& updater
|
||||||
|
|
||||||
void AutoUpdaterDialog::cleanupAfterUpdate()
|
void AutoUpdaterDialog::cleanupAfterUpdate()
|
||||||
{
|
{
|
||||||
|
// If we weren't portable, then updater executable gets left in the application directory.
|
||||||
|
if (EmuFolders::AppRoot == EmuFolders::DataRoot)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const std::string updater_path = Path::Combine(EmuFolders::DataRoot, UPDATER_EXECUTABLE);
|
||||||
|
if (!FileSystem::FileExists(updater_path.c_str()))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!FileSystem::DeleteFile(updater_path.c_str()))
|
||||||
|
{
|
||||||
|
QMessageBox::critical(nullptr, tr("Updater Error"), tr("Failed to remove updater exe after update."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#elif defined(__APPLE__)
|
#elif defined(__APPLE__)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
|
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
|
||||||
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
@ -15,6 +15,7 @@
|
||||||
#include <QtCore/QTimer>
|
#include <QtCore/QTimer>
|
||||||
#include <QtWidgets/QDialog>
|
#include <QtWidgets/QDialog>
|
||||||
|
|
||||||
|
class Error;
|
||||||
class HTTPDownloader;
|
class HTTPDownloader;
|
||||||
|
|
||||||
class EmuThread;
|
class EmuThread;
|
||||||
|
@ -60,12 +61,12 @@ private:
|
||||||
void queueGetChanges();
|
void queueGetChanges();
|
||||||
void getChangesComplete(s32 status_code, std::vector<u8> response);
|
void getChangesComplete(s32 status_code, std::vector<u8> response);
|
||||||
|
|
||||||
|
bool processUpdate(const std::vector<u8>& update_data);
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
bool processUpdate(const std::vector<u8>& update_data);
|
bool doesUpdaterNeedElevation(const std::string& application_dir) const;
|
||||||
bool extractUpdater(const QString& zip_path, const QString& destination_path);
|
bool doUpdate(const std::string& application_dir, const std::string& zip_path, const std::string& updater_path);
|
||||||
bool doUpdate(const QString& zip_path, const QString& updater_path, const QString& destination_path);
|
bool extractUpdater(const std::string& zip_path, const std::string& destination_path, Error* error);
|
||||||
#else
|
|
||||||
bool processUpdate(const std::vector<u8>& update_data);
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Ui::AutoUpdaterDialog m_ui;
|
Ui::AutoUpdaterDialog m_ui;
|
||||||
|
|
|
@ -96,6 +96,6 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLi
|
||||||
|
|
||||||
progress.DisplayFormattedInformation("Launching '%s'...",
|
progress.DisplayFormattedInformation("Launching '%s'...",
|
||||||
StringUtil::WideStringToUTF8String(program_to_launch).c_str());
|
StringUtil::WideStringToUTF8String(program_to_launch).c_str());
|
||||||
ShellExecuteW(nullptr, L"open", program_to_launch.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
ShellExecuteW(nullptr, L"open", program_to_launch.c_str(), L"-updatecleanup", nullptr, SW_SHOWNORMAL);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue