First Commit
This commit is contained in:
31
updater/CMakeLists.txt
Normal file
31
updater/CMakeLists.txt
Normal file
@@ -0,0 +1,31 @@
|
||||
add_executable(updater
|
||||
SZErrors.h
|
||||
Updater.cpp
|
||||
Updater.h
|
||||
UpdaterExtractor.h
|
||||
)
|
||||
|
||||
target_link_libraries(updater PRIVATE common fmt::fmt)
|
||||
target_include_directories(updater PRIVATE .)
|
||||
|
||||
if(WIN32)
|
||||
target_sources(updater PRIVATE ../pcsx2-qt/VCRuntimeChecker.cpp)
|
||||
target_link_libraries(updater PRIVATE
|
||||
LZMA::LZMA
|
||||
Comctl32.lib
|
||||
)
|
||||
target_sources(updater PRIVATE
|
||||
Windows/WindowsUpdater.cpp
|
||||
Windows/resource.h
|
||||
Windows/updater.rc
|
||||
Windows/updater.manifest
|
||||
)
|
||||
set_target_properties(updater PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
endif()
|
||||
|
||||
if(NOT PACKAGE_MODE)
|
||||
install(TARGETS updater DESTINATION ${CMAKE_SOURCE_DIR}/bin)
|
||||
if(MSVC)
|
||||
install(FILES $<TARGET_PDB_FILE:updater> DESTINATION ${CMAKE_SOURCE_DIR}/bin)
|
||||
endif()
|
||||
endif()
|
||||
31
updater/SZErrors.h
Normal file
31
updater/SZErrors.h
Normal file
@@ -0,0 +1,31 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "7zTypes.h"
|
||||
|
||||
static inline const char* SZErrorToString(SRes res)
|
||||
{
|
||||
// clang-format off
|
||||
switch (res)
|
||||
{
|
||||
case SZ_OK: return "SZ_OK";
|
||||
case SZ_ERROR_DATA: return "SZ_ERROR_DATA";
|
||||
case SZ_ERROR_MEM: return "SZ_ERROR_MEM";
|
||||
case SZ_ERROR_CRC: return "SZ_ERROR_CRC";
|
||||
case SZ_ERROR_UNSUPPORTED: return "SZ_ERROR_UNSUPPORTED";
|
||||
case SZ_ERROR_PARAM: return "SZ_ERROR_PARAM";
|
||||
case SZ_ERROR_INPUT_EOF: return "SZ_ERROR_INPUT_EOF";
|
||||
case SZ_ERROR_OUTPUT_EOF: return "SZ_ERROR_OUTPUT_EOF";
|
||||
case SZ_ERROR_READ: return "SZ_ERROR_READ";
|
||||
case SZ_ERROR_WRITE: return "SZ_ERROR_WRITE";
|
||||
case SZ_ERROR_PROGRESS: return "SZ_ERROR_PROGRESS";
|
||||
case SZ_ERROR_FAIL: return "SZ_ERROR_FAIL";
|
||||
case SZ_ERROR_THREAD: return "SZ_ERROR_THREAD";
|
||||
case SZ_ERROR_ARCHIVE: return "SZ_ERROR_ARCHIVE";
|
||||
case SZ_ERROR_NO_ARCHIVE: return "SZ_ERROR_NO_ARCHIVE";
|
||||
default: return "SZ_UNKNOWN";
|
||||
}
|
||||
// clang-format on
|
||||
}
|
||||
432
updater/Updater.cpp
Normal file
432
updater/Updater.cpp
Normal file
@@ -0,0 +1,432 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "Updater.h"
|
||||
|
||||
#include "common/Console.h"
|
||||
#include "common/FileSystem.h"
|
||||
#include "common/Path.h"
|
||||
#include "common/ScopedGuard.h"
|
||||
#include "common/StringUtil.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include "common/RedtapeWilCom.h"
|
||||
#include <Shobjidl.h>
|
||||
#include <shellapi.h>
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
#include "7zAlloc.h"
|
||||
#include "7zCrc.h"
|
||||
#include "SZErrors.h"
|
||||
|
||||
static constexpr size_t kInputBufSize = ((size_t)1 << 18);
|
||||
static constexpr ISzAlloc g_Alloc = {SzAlloc, SzFree};
|
||||
#endif
|
||||
|
||||
Updater::Updater(ProgressCallback* progress)
|
||||
: m_progress(progress)
|
||||
{
|
||||
progress->SetTitle("PCSX2 Update Installer");
|
||||
}
|
||||
|
||||
Updater::~Updater()
|
||||
{
|
||||
CloseUpdateZip();
|
||||
}
|
||||
|
||||
void Updater::SetupLogging(ProgressCallback* progress, const std::string& destination_directory)
|
||||
{
|
||||
Log::SetDebugOutputLevel(LOGLEVEL_DEBUG);
|
||||
|
||||
std::string log_path = Path::Combine(destination_directory, "updater.log");
|
||||
if (!Log::SetFileOutputLevel(LOGLEVEL_DEBUG, std::move(log_path)))
|
||||
progress->DisplayFormattedModalError("Failed to open log file '%s'", log_path.c_str());
|
||||
}
|
||||
|
||||
bool Updater::Initialize(std::string destination_directory)
|
||||
{
|
||||
m_destination_directory = std::move(destination_directory);
|
||||
m_staging_directory = StringUtil::StdStringFromFormat("%s" FS_OSPATH_SEPARATOR_STR "%s",
|
||||
m_destination_directory.c_str(), "UPDATE_STAGING");
|
||||
m_progress->DisplayFormattedInformation("Destination directory: '%s'", m_destination_directory.c_str());
|
||||
m_progress->DisplayFormattedInformation("Staging directory: '%s'", m_staging_directory.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Updater::OpenUpdateZip(const char* path)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
FileInStream_CreateVTable(&m_archive_stream);
|
||||
LookToRead2_CreateVTable(&m_look_stream, False);
|
||||
CrcGenerateTable();
|
||||
|
||||
m_zip_path = path;
|
||||
|
||||
m_look_stream.buf = (Byte*)ISzAlloc_Alloc(&g_Alloc, kInputBufSize);
|
||||
if (!m_look_stream.buf)
|
||||
{
|
||||
m_progress->DisplayFormattedError("Failed to allocate input buffer?!");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_look_stream.bufSize = kInputBufSize;
|
||||
m_look_stream.realStream = &m_archive_stream.vt;
|
||||
LookToRead2_INIT(&m_look_stream);
|
||||
|
||||
#ifdef _WIN32
|
||||
WRes wres = InFile_OpenW(&m_archive_stream.file, FileSystem::GetWin32Path(path).c_str());
|
||||
#else
|
||||
WRes wres = InFile_Open(&m_archive_stream.file, path);
|
||||
#endif
|
||||
if (wres != 0)
|
||||
{
|
||||
m_progress->DisplayFormattedModalError("Failed to open '%s': %d", path, wres);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_file_opened = true;
|
||||
SzArEx_Init(&m_archive);
|
||||
|
||||
SRes res = SzArEx_Open(&m_archive, &m_look_stream.vt, &g_Alloc, &g_Alloc);
|
||||
if (res != SZ_OK)
|
||||
{
|
||||
m_progress->DisplayFormattedModalError("SzArEx_Open() failed: %s [%d]", SZErrorToString(res), res);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_archive_opened = true;
|
||||
m_progress->SetStatusText("Parsing update zip...");
|
||||
return ParseZip();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Updater::CloseUpdateZip()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (m_archive_opened)
|
||||
{
|
||||
SzArEx_Free(&m_archive, &g_Alloc);
|
||||
m_archive_opened = false;
|
||||
}
|
||||
|
||||
if (m_look_stream.buf)
|
||||
{
|
||||
ISzAlloc_Free(&g_Alloc, m_look_stream.buf);
|
||||
m_look_stream.buf = nullptr;
|
||||
}
|
||||
|
||||
if (m_file_opened)
|
||||
{
|
||||
File_Close(&m_archive_stream.file);
|
||||
m_file_opened = false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Updater::RecursiveDeleteDirectory(const char* path)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
wil::com_ptr_nothrow<IFileOperation> fo;
|
||||
HRESULT hr = CoCreateInstance(CLSID_FileOperation, NULL, CLSCTX_ALL, IID_PPV_ARGS(fo.put()));
|
||||
if (FAILED(hr))
|
||||
{
|
||||
m_progress->DisplayFormattedError("CoCreateInstance() for IFileOperation failed: %08X", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
wil::com_ptr_nothrow<IShellItem> item;
|
||||
hr = SHCreateItemFromParsingName(StringUtil::UTF8StringToWideString(path).c_str(), NULL, IID_PPV_ARGS(item.put()));
|
||||
if (FAILED(hr))
|
||||
{
|
||||
m_progress->DisplayFormattedError("SHCreateItemFromParsingName() for delete failed: %08X", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = fo->SetOperationFlags(FOF_NOCONFIRMATION | FOF_SILENT);
|
||||
if (FAILED(hr))
|
||||
m_progress->DisplayFormattedWarning("IFileOperation::SetOperationFlags() failed: %08X", hr);
|
||||
|
||||
hr = fo->DeleteItem(item.get(), nullptr);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
m_progress->DisplayFormattedError("IFileOperation::DeleteItem() failed: %08X", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
item.reset();
|
||||
hr = fo->PerformOperations();
|
||||
if (FAILED(hr))
|
||||
{
|
||||
m_progress->DisplayFormattedError("IFileOperation::PerformOperations() failed: %08X", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
#else
|
||||
return FileSystem::RecursiveDeleteDirectory(path);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Updater::ParseZip()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
std::vector<UInt16> filename_buffer;
|
||||
|
||||
for (u32 file_index = 0; file_index < m_archive.NumFiles; file_index++)
|
||||
{
|
||||
// skip directories, we handle them ourselves
|
||||
if (SzArEx_IsDir(&m_archive, file_index))
|
||||
continue;
|
||||
|
||||
size_t filename_len = SzArEx_GetFileNameUtf16(&m_archive, file_index, nullptr);
|
||||
if (filename_len <= 1)
|
||||
continue;
|
||||
|
||||
filename_buffer.resize(filename_len);
|
||||
SzArEx_GetFileNameUtf16(&m_archive, file_index, filename_buffer.data());
|
||||
|
||||
// TODO: This won't work on Linux (4-byte wchar_t).
|
||||
FileToUpdate entry;
|
||||
entry.file_index = file_index;
|
||||
entry.destination_filename = StringUtil::WideStringToUTF8String(reinterpret_cast<wchar_t*>(filename_buffer.data()));
|
||||
if (entry.destination_filename.empty())
|
||||
continue;
|
||||
|
||||
// replace forward slashes with backslashes
|
||||
for (size_t i = 0; i < entry.destination_filename.length(); i++)
|
||||
{
|
||||
if (entry.destination_filename[i] == '/' || entry.destination_filename[i] == '\\')
|
||||
entry.destination_filename[i] = FS_OSPATH_SEPARATOR_CHARACTER;
|
||||
}
|
||||
|
||||
// should never have a leading slash. just in case.
|
||||
while (entry.destination_filename[0] == FS_OSPATH_SEPARATOR_CHARACTER)
|
||||
entry.destination_filename.erase(0, 1);
|
||||
|
||||
// skip directories (we sort them out later)
|
||||
if (!entry.destination_filename.empty() && entry.destination_filename.back() != FS_OSPATH_SEPARATOR_CHARACTER)
|
||||
{
|
||||
// skip updater itself, since it was already pre-extracted.
|
||||
// also skips portable.ini to not mess with future non-portable installs.
|
||||
if (StringUtil::Strcasecmp(entry.destination_filename.c_str(), "updater.exe") != 0)
|
||||
{
|
||||
m_progress->DisplayFormattedInformation("Found file in zip: '%s'", entry.destination_filename.c_str());
|
||||
m_update_paths.push_back(std::move(entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_update_paths.empty())
|
||||
{
|
||||
m_progress->ModalError("No files found in update zip.");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const FileToUpdate& ftu : m_update_paths)
|
||||
{
|
||||
const size_t len = ftu.destination_filename.length();
|
||||
for (size_t i = 0; i < len; i++)
|
||||
{
|
||||
if (ftu.destination_filename[i] == FS_OSPATH_SEPARATOR_CHARACTER)
|
||||
{
|
||||
std::string dir(ftu.destination_filename.begin(), ftu.destination_filename.begin() + i);
|
||||
while (!dir.empty() && dir[dir.length() - 1] == FS_OSPATH_SEPARATOR_CHARACTER)
|
||||
dir.erase(dir.length() - 1);
|
||||
|
||||
if (std::find(m_update_directories.begin(), m_update_directories.end(), dir) == m_update_directories.end())
|
||||
m_update_directories.push_back(std::move(dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(m_update_directories.begin(), m_update_directories.end());
|
||||
for (const std::string& dir : m_update_directories)
|
||||
m_progress->DisplayFormattedDebugMessage("Directory: %s", dir.c_str());
|
||||
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Updater::PrepareStagingDirectory()
|
||||
{
|
||||
if (FileSystem::DirectoryExists(m_staging_directory.c_str()))
|
||||
{
|
||||
m_progress->DisplayFormattedWarning("Update staging directory already exists, removing");
|
||||
if (!RecursiveDeleteDirectory(m_staging_directory.c_str()) ||
|
||||
FileSystem::DirectoryExists(m_staging_directory.c_str()))
|
||||
{
|
||||
m_progress->ModalError("Failed to remove old staging directory");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!FileSystem::CreateDirectoryPath(m_staging_directory.c_str(), false))
|
||||
{
|
||||
m_progress->DisplayFormattedModalError("Failed to create staging directory %s", m_staging_directory.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// create subdirectories in staging directory
|
||||
for (const std::string& subdir : m_update_directories)
|
||||
{
|
||||
m_progress->DisplayFormattedInformation("Creating subdirectory in staging: %s", subdir.c_str());
|
||||
|
||||
const std::string staging_subdir =
|
||||
StringUtil::StdStringFromFormat("%s" FS_OSPATH_SEPARATOR_STR "%s", m_staging_directory.c_str(), subdir.c_str());
|
||||
if (!FileSystem::CreateDirectoryPath(staging_subdir.c_str(), false))
|
||||
{
|
||||
m_progress->DisplayFormattedModalError("Failed to create staging subdirectory %s", staging_subdir.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Updater::StageUpdate()
|
||||
{
|
||||
m_progress->SetProgressRange(static_cast<u32>(m_update_paths.size()));
|
||||
m_progress->SetProgressValue(0);
|
||||
|
||||
#ifdef _WIN32
|
||||
UInt32 block_index = 0xFFFFFFFF; /* it can have any value before first call (if outBuffer = 0) */
|
||||
Byte* out_buffer = 0; /* it must be 0 before first call for each new archive. */
|
||||
size_t out_buffer_size = 0; /* it can have any value before first call (if outBuffer = 0) */
|
||||
ScopedGuard out_buffer_guard([&out_buffer]() {
|
||||
if (out_buffer)
|
||||
ISzAlloc_Free(&g_Alloc, out_buffer);
|
||||
});
|
||||
|
||||
for (const FileToUpdate& ftu : m_update_paths)
|
||||
{
|
||||
m_progress->SetFormattedStatusText("Extracting '%s'...", ftu.destination_filename.c_str());
|
||||
m_progress->DisplayFormattedInformation("Decompressing '%s'...", ftu.destination_filename.c_str());
|
||||
|
||||
size_t out_offset = 0;
|
||||
size_t extracted_size = 0;
|
||||
SRes res = SzArEx_Extract(&m_archive, &m_look_stream.vt, ftu.file_index,
|
||||
&block_index, &out_buffer, &out_buffer_size, &out_offset, &extracted_size, &g_Alloc, &g_Alloc);
|
||||
if (res != SZ_OK)
|
||||
{
|
||||
m_progress->DisplayFormattedModalError("Failed to decompress file '%s' from 7z (file index=%u, error=%s)",
|
||||
ftu.destination_filename.c_str(), ftu.file_index, SZErrorToString(res));
|
||||
return false;
|
||||
}
|
||||
|
||||
m_progress->DisplayFormattedInformation("Writing '%s' to staging (%zu bytes)...", ftu.destination_filename.c_str(), extracted_size);
|
||||
|
||||
const std::string destination_file = StringUtil::StdStringFromFormat(
|
||||
"%s" FS_OSPATH_SEPARATOR_STR "%s", m_staging_directory.c_str(), ftu.destination_filename.c_str());
|
||||
std::FILE* fp = FileSystem::OpenCFile(destination_file.c_str(), "wb");
|
||||
if (!fp)
|
||||
{
|
||||
m_progress->DisplayFormattedModalError("Failed to open staging output file '%s'", destination_file.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool wrote_completely = std::fwrite(out_buffer + out_offset, extracted_size, 1, fp) == 1 && std::fflush(fp) == 0;
|
||||
if (std::fclose(fp) != 0 || !wrote_completely)
|
||||
{
|
||||
m_progress->DisplayFormattedModalError("Failed to write output file '%s'", destination_file.c_str());
|
||||
FileSystem::DeleteFilePath(destination_file.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
m_progress->IncrementProgressValue();
|
||||
}
|
||||
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Updater::CommitUpdate()
|
||||
{
|
||||
m_progress->SetStatusText("Committing update...");
|
||||
|
||||
// create directories in target
|
||||
for (const std::string& subdir : m_update_directories)
|
||||
{
|
||||
const std::string dest_subdir = StringUtil::StdStringFromFormat("%s" FS_OSPATH_SEPARATOR_STR "%s",
|
||||
m_destination_directory.c_str(), subdir.c_str());
|
||||
|
||||
if (!FileSystem::DirectoryExists(dest_subdir.c_str()) && !FileSystem::CreateDirectoryPath(dest_subdir.c_str(), false))
|
||||
{
|
||||
m_progress->DisplayFormattedModalError("Failed to create target directory '%s'", dest_subdir.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// move files to target
|
||||
for (const FileToUpdate& ftu : m_update_paths)
|
||||
{
|
||||
const std::string staging_file_name = StringUtil::StdStringFromFormat(
|
||||
"%s" FS_OSPATH_SEPARATOR_STR "%s", m_staging_directory.c_str(), ftu.destination_filename.c_str());
|
||||
const std::string dest_file_name = StringUtil::StdStringFromFormat(
|
||||
"%s" FS_OSPATH_SEPARATOR_STR "%s", m_destination_directory.c_str(), ftu.destination_filename.c_str());
|
||||
m_progress->DisplayFormattedInformation("Moving '%s' to '%s'", staging_file_name.c_str(), dest_file_name.c_str());
|
||||
#ifdef _WIN32
|
||||
const bool result =
|
||||
MoveFileExW(FileSystem::GetWin32Path(staging_file_name).c_str(),
|
||||
FileSystem::GetWin32Path(dest_file_name).c_str(), MOVEFILE_REPLACE_EXISTING);
|
||||
#else
|
||||
const bool result = (rename(staging_file_name.c_str(), dest_file_name.c_str()) == 0);
|
||||
#endif
|
||||
if (!result)
|
||||
{
|
||||
m_progress->DisplayFormattedModalError("Failed to rename '%s' to '%s'", staging_file_name.c_str(),
|
||||
dest_file_name.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Updater::CleanupStagingDirectory()
|
||||
{
|
||||
// remove staging directory itself
|
||||
if (!RecursiveDeleteDirectory(m_staging_directory.c_str()))
|
||||
m_progress->DisplayFormattedError("Failed to remove staging directory '%s'", m_staging_directory.c_str());
|
||||
}
|
||||
|
||||
void Updater::RemoveUpdateZip()
|
||||
{
|
||||
if (m_zip_path.empty())
|
||||
return;
|
||||
|
||||
CloseUpdateZip();
|
||||
|
||||
if (!FileSystem::DeleteFilePath(m_zip_path.c_str()))
|
||||
m_progress->DisplayFormattedError("Failed to remove update zip '%s'", m_zip_path.c_str());
|
||||
}
|
||||
|
||||
std::string Updater::FindPCSX2Exe() const
|
||||
{
|
||||
for (const FileToUpdate& file : m_update_paths)
|
||||
{
|
||||
const std::string& name = file.destination_filename;
|
||||
if (name.find(FS_OSPATH_SEPARATOR_CHARACTER) != name.npos)
|
||||
continue; // Main exe is expected to be at the top level
|
||||
if (!StringUtil::StartsWithNoCase(name, "pcsx2"))
|
||||
continue;
|
||||
if (!StringUtil::EndsWithNoCase(name, "exe"))
|
||||
continue;
|
||||
return name;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
66
updater/Updater.h
Normal file
66
updater/Updater.h
Normal file
@@ -0,0 +1,66 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/ProgressCallback.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include "common/RedtapeWindows.h"
|
||||
#include "7z.h"
|
||||
#include "7zFile.h"
|
||||
#endif
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class Updater
|
||||
{
|
||||
public:
|
||||
Updater(ProgressCallback* progress);
|
||||
~Updater();
|
||||
|
||||
static void SetupLogging(ProgressCallback* progress, const std::string& destination_directory);
|
||||
|
||||
bool Initialize(std::string destination_directory);
|
||||
|
||||
bool OpenUpdateZip(const char* path);
|
||||
bool PrepareStagingDirectory();
|
||||
bool StageUpdate();
|
||||
bool CommitUpdate();
|
||||
void CleanupStagingDirectory();
|
||||
void RemoveUpdateZip();
|
||||
|
||||
std::string FindPCSX2Exe() const;
|
||||
|
||||
private:
|
||||
bool RecursiveDeleteDirectory(const char* path);
|
||||
|
||||
void CloseUpdateZip();
|
||||
|
||||
struct FileToUpdate
|
||||
{
|
||||
u32 file_index;
|
||||
std::string destination_filename;
|
||||
};
|
||||
|
||||
bool ParseZip();
|
||||
|
||||
std::string m_zip_path;
|
||||
std::string m_destination_directory;
|
||||
std::string m_staging_directory;
|
||||
|
||||
std::vector<FileToUpdate> m_update_paths;
|
||||
std::vector<std::string> m_update_directories;
|
||||
|
||||
ProgressCallback* m_progress;
|
||||
|
||||
#ifdef _WIN32
|
||||
CFileInStream m_archive_stream = {};
|
||||
CLookToRead2 m_look_stream = {};
|
||||
CSzArEx m_archive = {};
|
||||
|
||||
bool m_file_opened = false;
|
||||
bool m_archive_opened = false;
|
||||
#endif
|
||||
};
|
||||
153
updater/UpdaterExtractor.h
Normal file
153
updater/UpdaterExtractor.h
Normal file
@@ -0,0 +1,153 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/FileSystem.h"
|
||||
#include "common/ScopedGuard.h"
|
||||
#include "common/StringUtil.h"
|
||||
|
||||
#include "fmt/format.h"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include "7z.h"
|
||||
#include "7zAlloc.h"
|
||||
#include "7zCrc.h"
|
||||
#include "7zFile.h"
|
||||
#include "SZErrors.h"
|
||||
#endif
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
static constexpr char UPDATER_EXECUTABLE[] = "updater.exe";
|
||||
static constexpr char UPDATER_ARCHIVE_NAME[] = "update.7z";
|
||||
#endif
|
||||
|
||||
static inline bool ExtractUpdater(const char* archive_path, const char* destination_path, std::string* error)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
static constexpr size_t kInputBufSize = ((size_t)1 << 18);
|
||||
static constexpr ISzAlloc g_Alloc = {SzAlloc, SzFree};
|
||||
|
||||
CFileInStream instream = {};
|
||||
CLookToRead2 lookstream = {};
|
||||
CSzArEx archive = {};
|
||||
|
||||
FileInStream_CreateVTable(&instream);
|
||||
LookToRead2_CreateVTable(&lookstream, False);
|
||||
CrcGenerateTable();
|
||||
|
||||
lookstream.buf = (Byte*)ISzAlloc_Alloc(&g_Alloc, kInputBufSize);
|
||||
if (!lookstream.buf)
|
||||
{
|
||||
*error = "Failed to allocate input buffer?!";
|
||||
return false;
|
||||
}
|
||||
|
||||
lookstream.bufSize = kInputBufSize;
|
||||
lookstream.realStream = &instream.vt;
|
||||
LookToRead2_INIT(&lookstream);
|
||||
ScopedGuard buffer_guard([&lookstream]() {
|
||||
ISzAlloc_Free(&g_Alloc, lookstream.buf);
|
||||
});
|
||||
|
||||
#ifdef _WIN32
|
||||
WRes wres = InFile_OpenW(&instream.file, FileSystem::GetWin32Path(archive_path).c_str());
|
||||
#else
|
||||
WRes wres = InFile_Open(&instream.file, archive_path);
|
||||
#endif
|
||||
if (wres != 0)
|
||||
{
|
||||
*error = fmt::format("Failed to open '{0}': {1}", archive_path, wres);
|
||||
return false;
|
||||
}
|
||||
|
||||
ScopedGuard file_guard([&instream]() {
|
||||
File_Close(&instream.file);
|
||||
});
|
||||
|
||||
SzArEx_Init(&archive);
|
||||
|
||||
SRes res = SzArEx_Open(&archive, &lookstream.vt, &g_Alloc, &g_Alloc);
|
||||
if (res != SZ_OK)
|
||||
{
|
||||
*error = fmt::format("SzArEx_Open() failed: {0} [{1}]", SZErrorToString(res), res);
|
||||
return false;
|
||||
}
|
||||
ScopedGuard archive_guard([&archive]() {
|
||||
SzArEx_Free(&archive, &g_Alloc);
|
||||
});
|
||||
|
||||
std::vector<UInt16> filename_buffer;
|
||||
u32 updater_file_index = archive.NumFiles;
|
||||
for (u32 file_index = 0; file_index < archive.NumFiles; file_index++)
|
||||
{
|
||||
if (SzArEx_IsDir(&archive, file_index))
|
||||
continue;
|
||||
|
||||
size_t filename_len = SzArEx_GetFileNameUtf16(&archive, file_index, nullptr);
|
||||
if (filename_len <= 1)
|
||||
continue;
|
||||
|
||||
filename_buffer.resize(filename_len);
|
||||
filename_len = SzArEx_GetFileNameUtf16(&archive, file_index, filename_buffer.data());
|
||||
|
||||
// TODO: This won't work on Linux (4-byte wchar_t).
|
||||
const std::string filename(StringUtil::WideStringToUTF8String(reinterpret_cast<wchar_t*>(filename_buffer.data())));
|
||||
if (filename != UPDATER_EXECUTABLE)
|
||||
continue;
|
||||
|
||||
updater_file_index = file_index;
|
||||
break;
|
||||
}
|
||||
|
||||
if (updater_file_index == archive.NumFiles)
|
||||
{
|
||||
*error = fmt::format("Updater executable ({}) not found in archive.", UPDATER_EXECUTABLE);
|
||||
return false;
|
||||
}
|
||||
|
||||
UInt32 block_index = 0xFFFFFFFF; /* it can have any value before first call (if outBuffer = 0) */
|
||||
Byte* out_buffer = 0; /* it must be 0 before first call for each new archive. */
|
||||
size_t out_buffer_size = 0; /* it can have any value before first call (if outBuffer = 0) */
|
||||
ScopedGuard out_buffer_guard([&out_buffer]() {
|
||||
if (out_buffer)
|
||||
ISzAlloc_Free(&g_Alloc, out_buffer);
|
||||
});
|
||||
|
||||
size_t out_offset = 0;
|
||||
size_t extracted_size = 0;
|
||||
res = SzArEx_Extract(&archive, &lookstream.vt, updater_file_index,
|
||||
&block_index, &out_buffer, &out_buffer_size, &out_offset, &extracted_size, &g_Alloc, &g_Alloc);
|
||||
if (res != SZ_OK)
|
||||
{
|
||||
*error = fmt::format("Failed to decompress {0} from 7z (file index=%u, error=%s)",
|
||||
UPDATER_EXECUTABLE, updater_file_index, SZErrorToString(res));
|
||||
return false;
|
||||
}
|
||||
|
||||
std::FILE* fp = FileSystem::OpenCFile(destination_path, "wb");
|
||||
if (!fp)
|
||||
{
|
||||
*error = fmt::format("Failed to open '{0}' for writing.", destination_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool wrote_completely = std::fwrite(out_buffer + out_offset, extracted_size, 1, fp) == 1 && std::fflush(fp) == 0;
|
||||
if (std::fclose(fp) != 0 || !wrote_completely)
|
||||
{
|
||||
*error = fmt::format("Failed to write output file '{}'", destination_path);
|
||||
FileSystem::DeleteFilePath(destination_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
error->clear();
|
||||
return true;
|
||||
#else
|
||||
*error = "Not supported on this platform";
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
521
updater/Windows/WindowsUpdater.cpp
Normal file
521
updater/Windows/WindowsUpdater.cpp
Normal file
@@ -0,0 +1,521 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "Updater.h"
|
||||
#include "Windows/resource.h"
|
||||
|
||||
#include "common/FileSystem.h"
|
||||
#include "common/Console.h"
|
||||
#include "common/ScopedGuard.h"
|
||||
#include "common/StringUtil.h"
|
||||
#include "common/ProgressCallback.h"
|
||||
#include "common/RedtapeWilCom.h"
|
||||
|
||||
#include <CommCtrl.h>
|
||||
#include <shellapi.h>
|
||||
#include <Shobjidl.h>
|
||||
|
||||
#include <thread>
|
||||
|
||||
#include <wil/resource.h>
|
||||
#include <wil/win32_helpers.h>
|
||||
|
||||
#pragma comment(lib, "synchronization.lib")
|
||||
|
||||
class Win32ProgressCallback final : public BaseProgressCallback
|
||||
{
|
||||
public:
|
||||
Win32ProgressCallback();
|
||||
~Win32ProgressCallback() override;
|
||||
|
||||
void PushState() override;
|
||||
void PopState() override;
|
||||
|
||||
void SetCancellable(bool cancellable) override;
|
||||
void SetTitle(const char* title) override;
|
||||
void SetStatusText(const char* text) override;
|
||||
void SetProgressRange(u32 range) override;
|
||||
void SetProgressValue(u32 value) override;
|
||||
void SetProgressState(ProgressState state) override;
|
||||
|
||||
void DisplayError(const char* message) override;
|
||||
void DisplayWarning(const char* message) override;
|
||||
void DisplayInformation(const char* message) override;
|
||||
void DisplayDebugMessage(const char* message) override;
|
||||
|
||||
void ModalError(const char* message) override;
|
||||
bool ModalConfirmation(const char* message) override;
|
||||
void ModalInformation(const char* message) override;
|
||||
|
||||
private:
|
||||
enum : int
|
||||
{
|
||||
WINDOW_WIDTH = 600,
|
||||
WINDOW_HEIGHT = 300,
|
||||
WINDOW_MARGIN = 10,
|
||||
SUBWINDOW_WIDTH = WINDOW_WIDTH - 20 - WINDOW_MARGIN - WINDOW_MARGIN,
|
||||
};
|
||||
|
||||
bool Create();
|
||||
void Destroy();
|
||||
void Redraw(bool force);
|
||||
void PumpMessages();
|
||||
|
||||
static LRESULT CALLBACK WndProcThunk(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
|
||||
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
|
||||
|
||||
void UIThreadProc(wil::slim_event_manual_reset* creationEvent);
|
||||
|
||||
std::thread m_ui_thread;
|
||||
|
||||
HWND m_window_hwnd{};
|
||||
HWND m_text_hwnd{};
|
||||
HWND m_progress_hwnd{};
|
||||
HWND m_list_box_hwnd{};
|
||||
|
||||
wil::com_ptr_nothrow<ITaskbarList3> m_taskbar_list;
|
||||
|
||||
int m_last_progress_percent = -1;
|
||||
bool m_com_initialized = false;
|
||||
|
||||
static inline const UINT s_uTBBC = RegisterWindowMessageW(L"TaskbarButtonCreated");
|
||||
static constexpr UINT WMAPP_SETTASKBARPROGRESS = WM_USER+0;
|
||||
static constexpr UINT WMAPP_SETTASKBARSTATE = WM_USER+1;
|
||||
};
|
||||
|
||||
Win32ProgressCallback::Win32ProgressCallback()
|
||||
: BaseProgressCallback()
|
||||
{
|
||||
wil::slim_event_manual_reset creationEvent;
|
||||
m_ui_thread = std::thread(&Win32ProgressCallback::UIThreadProc, this, &creationEvent);
|
||||
creationEvent.wait();
|
||||
}
|
||||
|
||||
Win32ProgressCallback::~Win32ProgressCallback()
|
||||
{
|
||||
if (m_window_hwnd)
|
||||
PostMessageW(m_window_hwnd, WM_CLOSE, 0, 0);
|
||||
m_ui_thread.join();
|
||||
}
|
||||
|
||||
void Win32ProgressCallback::PushState()
|
||||
{
|
||||
BaseProgressCallback::PushState();
|
||||
}
|
||||
|
||||
void Win32ProgressCallback::PopState()
|
||||
{
|
||||
BaseProgressCallback::PopState();
|
||||
Redraw(true);
|
||||
}
|
||||
|
||||
void Win32ProgressCallback::SetCancellable(bool cancellable)
|
||||
{
|
||||
BaseProgressCallback::SetCancellable(cancellable);
|
||||
Redraw(true);
|
||||
}
|
||||
|
||||
void Win32ProgressCallback::SetTitle(const char* title)
|
||||
{
|
||||
SetWindowTextW(m_window_hwnd, StringUtil::UTF8StringToWideString(title).c_str());
|
||||
}
|
||||
|
||||
void Win32ProgressCallback::SetStatusText(const char* text)
|
||||
{
|
||||
BaseProgressCallback::SetStatusText(text);
|
||||
Redraw(true);
|
||||
}
|
||||
|
||||
void Win32ProgressCallback::SetProgressRange(u32 range)
|
||||
{
|
||||
BaseProgressCallback::SetProgressRange(range);
|
||||
Redraw(false);
|
||||
}
|
||||
|
||||
void Win32ProgressCallback::SetProgressValue(u32 value)
|
||||
{
|
||||
BaseProgressCallback::SetProgressValue(value);
|
||||
Redraw(false);
|
||||
}
|
||||
|
||||
void Win32ProgressCallback::SetProgressState(ProgressState state)
|
||||
{
|
||||
BaseProgressCallback::SetProgressState(state);
|
||||
Redraw(true);
|
||||
}
|
||||
|
||||
void Win32ProgressCallback::UIThreadProc(wil::slim_event_manual_reset* creationEvent)
|
||||
{
|
||||
if (Create())
|
||||
{
|
||||
creationEvent->SetEvent();
|
||||
|
||||
MSG msg;
|
||||
while (GetMessageW(&msg, m_window_hwnd, 0, 0) > 0)
|
||||
{
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
Destroy();
|
||||
}
|
||||
else
|
||||
{
|
||||
creationEvent->SetEvent();
|
||||
}
|
||||
}
|
||||
|
||||
bool Win32ProgressCallback::Create()
|
||||
{
|
||||
static const wchar_t* CLASS_NAME = L"PCSX2Win32ProgressCallbackWindow";
|
||||
static bool class_registered = false;
|
||||
|
||||
if (!class_registered)
|
||||
{
|
||||
InitCommonControls();
|
||||
|
||||
WNDCLASSEX wc = {sizeof(wc)};
|
||||
wc.lpfnWndProc = WndProcThunk;
|
||||
wc.hInstance = wil::GetModuleInstanceHandle();
|
||||
wc.hIcon = LoadIcon(wc.hInstance, MAKEINTRESOURCE(IDI_ICON1));
|
||||
wc.hIconSm = LoadIcon(wc.hInstance, MAKEINTRESOURCE(IDI_ICON1));
|
||||
wc.hCursor = LoadCursor(NULL, IDC_WAIT);
|
||||
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
|
||||
wc.style = CS_NOCLOSE;
|
||||
wc.lpszClassName = CLASS_NAME;
|
||||
if (!RegisterClassExW(&wc))
|
||||
{
|
||||
MessageBoxW(nullptr, L"Failed to register window class", L"Error", MB_OK);
|
||||
return false;
|
||||
}
|
||||
|
||||
class_registered = true;
|
||||
}
|
||||
|
||||
m_com_initialized = SUCCEEDED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED));
|
||||
|
||||
m_window_hwnd =
|
||||
CreateWindowExW(WS_EX_CLIENTEDGE, CLASS_NAME, L"Win32ProgressCallback", WS_OVERLAPPEDWINDOW & ~WS_MAXIMIZEBOX, CW_USEDEFAULT,
|
||||
CW_USEDEFAULT, WINDOW_WIDTH, WINDOW_HEIGHT, nullptr, nullptr, wil::GetModuleInstanceHandle(), this);
|
||||
if (!m_window_hwnd)
|
||||
{
|
||||
MessageBoxW(nullptr, L"Failed to create window", L"Error", MB_OK);
|
||||
return false;
|
||||
}
|
||||
|
||||
ShowWindow(m_window_hwnd, SW_SHOW);
|
||||
|
||||
// Pump messages manually just this one time to give the chance for the taskbar icon to be created etc.
|
||||
PumpMessages();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Win32ProgressCallback::Destroy()
|
||||
{
|
||||
m_taskbar_list.reset();
|
||||
|
||||
if (m_window_hwnd)
|
||||
{
|
||||
DestroyWindow(m_window_hwnd);
|
||||
m_window_hwnd = {};
|
||||
m_text_hwnd = {};
|
||||
m_progress_hwnd = {};
|
||||
}
|
||||
|
||||
if (m_com_initialized)
|
||||
CoUninitialize();
|
||||
}
|
||||
|
||||
void Win32ProgressCallback::PumpMessages()
|
||||
{
|
||||
MSG msg;
|
||||
while (PeekMessageW(&msg, m_window_hwnd, 0, 0, PM_REMOVE))
|
||||
{
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
}
|
||||
|
||||
void Win32ProgressCallback::Redraw(bool force)
|
||||
{
|
||||
const int percent =
|
||||
static_cast<int>((static_cast<float>(m_progress_value) / static_cast<float>(m_progress_range)) * 100.0f);
|
||||
if (percent == m_last_progress_percent && !force)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_last_progress_percent = percent;
|
||||
|
||||
const LONG_PTR style = GetWindowLongPtrW(m_progress_hwnd, GWL_STYLE);
|
||||
LONG_PTR newStyle = style;
|
||||
if (m_progress_state == ProgressState::Normal)
|
||||
{
|
||||
newStyle = style & ~(PBS_MARQUEE);
|
||||
SendMessageW(m_window_hwnd, WMAPP_SETTASKBARPROGRESS, m_progress_value, m_progress_range);
|
||||
}
|
||||
else if (m_progress_state == ProgressState::Indeterminate)
|
||||
{
|
||||
newStyle = style | PBS_MARQUEE;
|
||||
SendMessageW(m_window_hwnd, WMAPP_SETTASKBARSTATE, TBPF_INDETERMINATE, 0);
|
||||
}
|
||||
|
||||
if (style != newStyle)
|
||||
{
|
||||
SetWindowLongPtrW(m_progress_hwnd, GWL_STYLE, newStyle);
|
||||
SendMessageW(m_progress_hwnd, PBM_SETMARQUEE, m_progress_state == ProgressState::Indeterminate, 0);
|
||||
}
|
||||
if (m_progress_state != ProgressState::Indeterminate)
|
||||
{
|
||||
SendMessageW(m_progress_hwnd, PBM_SETRANGE, 0, MAKELPARAM(0, m_progress_range));
|
||||
SendMessageW(m_progress_hwnd, PBM_SETPOS, static_cast<WPARAM>(m_progress_value), 0);
|
||||
}
|
||||
SetWindowTextW(m_text_hwnd, StringUtil::UTF8StringToWideString(m_status_text).c_str());
|
||||
}
|
||||
|
||||
LRESULT CALLBACK Win32ProgressCallback::WndProcThunk(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
|
||||
{
|
||||
Win32ProgressCallback* cb;
|
||||
if (msg == WM_CREATE)
|
||||
{
|
||||
const CREATESTRUCTW* cs = reinterpret_cast<CREATESTRUCTW*>(lparam);
|
||||
cb = static_cast<Win32ProgressCallback*>(cs->lpCreateParams);
|
||||
}
|
||||
else
|
||||
{
|
||||
cb = reinterpret_cast<Win32ProgressCallback*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||
}
|
||||
|
||||
return cb->WndProc(hwnd, msg, wparam, lparam);
|
||||
}
|
||||
|
||||
LRESULT CALLBACK Win32ProgressCallback::WndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
|
||||
{
|
||||
switch (msg)
|
||||
{
|
||||
case WM_CREATE:
|
||||
{
|
||||
const CREATESTRUCTA* cs = reinterpret_cast<CREATESTRUCTA*>(lparam);
|
||||
HFONT default_font = reinterpret_cast<HFONT>(GetStockObject(ANSI_VAR_FONT));
|
||||
SendMessageW(hwnd, WM_SETFONT, WPARAM(default_font), TRUE);
|
||||
|
||||
int y = WINDOW_MARGIN;
|
||||
|
||||
m_text_hwnd = CreateWindowExW(0, L"Static", nullptr, WS_VISIBLE | WS_CHILD, WINDOW_MARGIN, y, SUBWINDOW_WIDTH, 16,
|
||||
hwnd, nullptr, cs->hInstance, nullptr);
|
||||
SendMessageW(m_text_hwnd, WM_SETFONT, WPARAM(default_font), TRUE);
|
||||
y += 16 + WINDOW_MARGIN;
|
||||
|
||||
m_progress_hwnd = CreateWindowExW(0, PROGRESS_CLASSW, nullptr, WS_VISIBLE | WS_CHILD, WINDOW_MARGIN, y,
|
||||
SUBWINDOW_WIDTH, 32, hwnd, nullptr, cs->hInstance, nullptr);
|
||||
y += 32 + WINDOW_MARGIN;
|
||||
|
||||
m_list_box_hwnd =
|
||||
CreateWindowExW(0, L"LISTBOX", nullptr, WS_VISIBLE | WS_CHILD | WS_VSCROLL | WS_HSCROLL | WS_BORDER | LBS_NOSEL,
|
||||
WINDOW_MARGIN, y, SUBWINDOW_WIDTH, 170, hwnd, nullptr, cs->hInstance, nullptr);
|
||||
SendMessageW(m_list_box_hwnd, WM_SETFONT, WPARAM(default_font), TRUE);
|
||||
y += 170;
|
||||
|
||||
// In case the application is run elevated, allow the
|
||||
// TaskbarButtonCreated message through.
|
||||
ChangeWindowMessageFilterEx(hwnd, s_uTBBC, MSGFLT_ALLOW, nullptr);
|
||||
|
||||
SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_DESTROY:
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
|
||||
case WMAPP_SETTASKBARPROGRESS:
|
||||
{
|
||||
if (m_taskbar_list)
|
||||
m_taskbar_list->SetProgressValue(m_window_hwnd, wparam, lparam);
|
||||
}
|
||||
break;
|
||||
|
||||
case WMAPP_SETTASKBARSTATE:
|
||||
{
|
||||
if (m_taskbar_list)
|
||||
m_taskbar_list->SetProgressState(m_window_hwnd, static_cast<TBPFLAG>(wparam));
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (msg == s_uTBBC)
|
||||
{
|
||||
if (m_com_initialized)
|
||||
{
|
||||
m_taskbar_list = wil::CoCreateInstanceNoThrow<ITaskbarList3>(CLSID_TaskbarList);
|
||||
if (m_taskbar_list)
|
||||
{
|
||||
if (FAILED(m_taskbar_list->HrInit()))
|
||||
{
|
||||
m_taskbar_list.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProcW(hwnd, msg, wparam, lparam);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Win32ProgressCallback::DisplayError(const char* message)
|
||||
{
|
||||
Console.Error(message);
|
||||
SendMessageW(m_list_box_hwnd, LB_ADDSTRING, 0, reinterpret_cast<LPARAM>(StringUtil::UTF8StringToWideString(message).c_str()));
|
||||
SendMessageW(m_list_box_hwnd, WM_VSCROLL, SB_BOTTOM, 0);
|
||||
}
|
||||
|
||||
void Win32ProgressCallback::DisplayWarning(const char* message)
|
||||
{
|
||||
Console.Warning(message);
|
||||
SendMessageW(m_list_box_hwnd, LB_ADDSTRING, 0, reinterpret_cast<LPARAM>(StringUtil::UTF8StringToWideString(message).c_str()));
|
||||
SendMessageW(m_list_box_hwnd, WM_VSCROLL, SB_BOTTOM, 0);
|
||||
}
|
||||
|
||||
void Win32ProgressCallback::DisplayInformation(const char* message)
|
||||
{
|
||||
Console.WriteLn(message);
|
||||
SendMessageW(m_list_box_hwnd, LB_ADDSTRING, 0, reinterpret_cast<LPARAM>(StringUtil::UTF8StringToWideString(message).c_str()));
|
||||
SendMessageW(m_list_box_hwnd, WM_VSCROLL, SB_BOTTOM, 0);
|
||||
}
|
||||
|
||||
void Win32ProgressCallback::DisplayDebugMessage(const char* message)
|
||||
{
|
||||
Console.WriteLn(message);
|
||||
}
|
||||
|
||||
void Win32ProgressCallback::ModalError(const char* message)
|
||||
{
|
||||
MessageBoxW(m_window_hwnd, StringUtil::UTF8StringToWideString(message).c_str(), L"Error", MB_ICONERROR | MB_OK);
|
||||
}
|
||||
|
||||
bool Win32ProgressCallback::ModalConfirmation(const char* message)
|
||||
{
|
||||
return MessageBoxW(m_window_hwnd, StringUtil::UTF8StringToWideString(message).c_str(), L"Confirmation", MB_ICONQUESTION | MB_YESNO) == IDYES;
|
||||
}
|
||||
|
||||
void Win32ProgressCallback::ModalInformation(const char* message)
|
||||
{
|
||||
MessageBoxW(m_window_hwnd, StringUtil::UTF8StringToWideString(message).c_str(), L"Information", MB_ICONINFORMATION | MB_OK);
|
||||
}
|
||||
|
||||
static void WaitForProcessToExit(int process_id)
|
||||
{
|
||||
HANDLE hProcess = OpenProcess(SYNCHRONIZE, FALSE, process_id);
|
||||
if (!hProcess)
|
||||
return;
|
||||
|
||||
WaitForSingleObject(hProcess, INFINITE);
|
||||
CloseHandle(hProcess);
|
||||
}
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nShowCmd)
|
||||
{
|
||||
Win32ProgressCallback progress;
|
||||
|
||||
const bool com_initialized = SUCCEEDED(CoInitializeEx(nullptr, COINIT_MULTITHREADED));
|
||||
const ScopedGuard com_guard = [com_initialized]() {
|
||||
if (com_initialized)
|
||||
CoUninitialize();
|
||||
};
|
||||
|
||||
int argc = 0;
|
||||
wil::unique_hlocal_ptr<LPWSTR[]> argv(CommandLineToArgvW(GetCommandLineW(), &argc));
|
||||
if (!argv || argc <= 0)
|
||||
{
|
||||
progress.ModalError("Failed to parse command line.");
|
||||
return 1;
|
||||
}
|
||||
if (argc != 5)
|
||||
{
|
||||
progress.ModalError("Expected 4 arguments: parent process id, output directory, update zip, program to "
|
||||
"launch.\n\nThis program is not intended to be run manually, please use the main PCSX2 application and "
|
||||
"click Help->Check for Updates.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const int parent_process_id = StringUtil::FromChars<int>(StringUtil::WideStringToUTF8String(argv[1])).value_or(0);
|
||||
const std::string destination_directory = StringUtil::WideStringToUTF8String(argv[2]);
|
||||
const std::string zip_path = StringUtil::WideStringToUTF8String(argv[3]);
|
||||
const std::wstring program_to_launch(argv[4]);
|
||||
argv.reset();
|
||||
|
||||
if (parent_process_id <= 0 || destination_directory.empty() || zip_path.empty() || program_to_launch.empty())
|
||||
{
|
||||
progress.ModalError("One or more parameters is empty.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Updater::SetupLogging(&progress, destination_directory);
|
||||
Updater updater(&progress);
|
||||
|
||||
progress.SetFormattedStatusText("Waiting for parent process %d to exit...", parent_process_id);
|
||||
progress.SetProgressState(ProgressCallback::ProgressState::Indeterminate);
|
||||
WaitForProcessToExit(parent_process_id);
|
||||
|
||||
if (!updater.Initialize(destination_directory))
|
||||
{
|
||||
progress.ModalError("Failed to initialize updater.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!updater.OpenUpdateZip(zip_path.c_str()))
|
||||
{
|
||||
progress.DisplayFormattedModalError("Could not open update zip '%s'. Update not installed.", zip_path.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!updater.PrepareStagingDirectory())
|
||||
{
|
||||
progress.ModalError("Failed to prepare staging directory. Update not installed.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!updater.StageUpdate())
|
||||
{
|
||||
progress.ModalError("Failed to stage update. Update not installed.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!updater.CommitUpdate())
|
||||
{
|
||||
progress.ModalError(
|
||||
"Failed to commit update. Your installation may be corrupted, please re-download a fresh version from pcsx2.net.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
updater.CleanupStagingDirectory();
|
||||
updater.RemoveUpdateZip();
|
||||
|
||||
// Rename the new executable to match the existing one
|
||||
if (std::string actual_exe = updater.FindPCSX2Exe(); !actual_exe.empty())
|
||||
{
|
||||
const std::string full_path = destination_directory + FS_OSPATH_SEPARATOR_STR + actual_exe;
|
||||
progress.DisplayFormattedInformation("Moving '%s' to '%S'", full_path.c_str(), program_to_launch.c_str());
|
||||
const bool ok = MoveFileExW(FileSystem::GetWin32Path(full_path).c_str(),
|
||||
FileSystem::GetWin32Path(StringUtil::WideStringToUTF8String(program_to_launch)).c_str(),
|
||||
MOVEFILE_REPLACE_EXISTING);
|
||||
if (!ok)
|
||||
{
|
||||
progress.DisplayFormattedModalError("Failed to rename '%s' to %S", full_path.c_str(), program_to_launch.c_str());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
progress.ModalError("Couldn't find PCSX2 in update package, please re-download a fresh version from GitHub.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
progress.DisplayFormattedInformation("Launching '%s'...",
|
||||
StringUtil::WideStringToUTF8String(program_to_launch).c_str());
|
||||
ShellExecuteW(nullptr, L"open", program_to_launch.c_str(), L"-updatecleanup", nullptr, SW_SHOWNORMAL);
|
||||
return 0;
|
||||
}
|
||||
16
updater/Windows/resource.h
Normal file
16
updater/Windows/resource.h
Normal file
@@ -0,0 +1,16 @@
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by updater.rc
|
||||
//
|
||||
#define IDI_ICON1 102
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 103
|
||||
#define _APS_NEXT_COMMAND_VALUE 40001
|
||||
#define _APS_NEXT_CONTROL_VALUE 1001
|
||||
#define _APS_NEXT_SYMED_VALUE 101
|
||||
#endif
|
||||
#endif
|
||||
BIN
updater/Windows/updater.ico
Normal file
BIN
updater/Windows/updater.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
22
updater/Windows/updater.manifest
Normal file
22
updater/Windows/updater.manifest
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
processorArchitecture="*"
|
||||
name="net.pcsx2.updater"
|
||||
type="win32"
|
||||
/>
|
||||
<description>PCSX2 Updater</description>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
</assembly>
|
||||
110
updater/Windows/updater.rc
Normal file
110
updater/Windows/updater.rc
Normal file
@@ -0,0 +1,110 @@
|
||||
// Microsoft Visual C++ generated resource script.
|
||||
//
|
||||
#include "resource.h"
|
||||
|
||||
#define APSTUDIO_READONLY_SYMBOLS
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 2 resource.
|
||||
//
|
||||
#include "winres.h"
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#undef APSTUDIO_READONLY_SYMBOLS
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// English (Australia) resources
|
||||
|
||||
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENA)
|
||||
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_AUS
|
||||
#pragma code_page(1252)
|
||||
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TEXTINCLUDE
|
||||
//
|
||||
|
||||
1 TEXTINCLUDE
|
||||
BEGIN
|
||||
"resource.h\0"
|
||||
END
|
||||
|
||||
2 TEXTINCLUDE
|
||||
BEGIN
|
||||
"#include ""winres.h""\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
3 TEXTINCLUDE
|
||||
BEGIN
|
||||
"\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
#endif // APSTUDIO_INVOKED
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Version
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 1,0,0,1
|
||||
PRODUCTVERSION 1,0,0,1
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS 0x40004L
|
||||
FILETYPE 0x1L
|
||||
FILESUBTYPE 0x0L
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "0c0904b0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "PCSX2"
|
||||
VALUE "FileDescription", "PCSX2"
|
||||
VALUE "FileVersion", "2.0"
|
||||
VALUE "InternalName", "updater.exe"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2022 PCSX2 Dev Team"
|
||||
VALUE "OriginalFilename", "updater.exe"
|
||||
VALUE "ProductName", "PCSX2 Update Installer"
|
||||
VALUE "ProductVersion", "2.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0xc09, 1200
|
||||
END
|
||||
END
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Icon
|
||||
//
|
||||
|
||||
// Icon with lowest ID value placed first to ensure application icon
|
||||
// remains consistent on all systems.
|
||||
IDI_ICON1 ICON "updater.ico"
|
||||
|
||||
#endif // English (Australia) resources
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
|
||||
#ifndef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 3 resource.
|
||||
//
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#endif // not APSTUDIO_INVOKED
|
||||
|
||||
75
updater/updater.vcxproj
Normal file
75
updater/updater.vcxproj
Normal file
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(SolutionDir)common\vsprops\BaseProjectConfig.props" />
|
||||
<Import Project="$(SolutionDir)common\vsprops\WinSDK.props" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{90BBDC04-CC44-4006-B893-06A4FEA8ED47}</ProjectGuid>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
<PlatformToolset Condition="!$(Configuration.Contains(Clang))">$(DefaultPlatformToolset)</PlatformToolset>
|
||||
<PlatformToolset Condition="$(Configuration.Contains(Clang))">ClangCL</PlatformToolset>
|
||||
<WholeProgramOptimization Condition="$(Configuration.Contains(Release))">true</WholeProgramOptimization>
|
||||
<UseDebugLibraries Condition="$(Configuration.Contains(Debug))">true</UseDebugLibraries>
|
||||
<UseDebugLibraries Condition="!$(Configuration.Contains(Debug))">false</UseDebugLibraries>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings" />
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Project="$(SolutionDir)common\vsprops\common.props" />
|
||||
<Import Project="$(SolutionDir)common\vsprops\BaseProperties.props" />
|
||||
<Import Condition="$(Configuration.Contains(Debug))" Project="$(SolutionDir)common\vsprops\CodeGen_Debug.props" />
|
||||
<Import Condition="$(Configuration.Contains(Devel))" Project="$(SolutionDir)common\vsprops\CodeGen_Devel.props" />
|
||||
<Import Condition="$(Configuration.Contains(Release))" Project="$(SolutionDir)common\vsprops\CodeGen_Release.props" />
|
||||
<Import Condition="!$(Configuration.Contains(Release))" Project="$(SolutionDir)common\vsprops\IncrementalLinking.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
|
||||
<TargetName>updater$(BuildString)</TargetName>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>$(SolutionDir)3rdparty\fmt\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories>$(SolutionDir)3rdparty\lzma\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories>$(SolutionDir)3rdparty\fast_float\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories>$(SolutionDir)3rdparty\winwil\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="$(SolutionDir)3rdparty\fmt\fmt.vcxproj">
|
||||
<Project>{449ad25e-424a-4714-babc-68706cdcc33b}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="$(SolutionDir)3rdparty\lzma\lzma.vcxproj">
|
||||
<Project>{a4323327-3f2b-4271-83d9-7f9a3c66b6b2}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="$(SolutionDir)common\common.vcxproj">
|
||||
<Project>{4639972e-424e-4e13-8b07-ca403c481346}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="Updater.cpp" />
|
||||
<ClCompile Include="Windows\WindowsUpdater.cpp" />
|
||||
<ClCompile Include="$(SolutionDir)pcsx2-qt\VCRuntimeChecker.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="SZErrors.h" />
|
||||
<ClInclude Include="Updater.h" />
|
||||
<ClInclude Include="UpdaterExtractor.h" />
|
||||
<ClInclude Include="Windows\resource.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Manifest Include="Windows\updater.manifest" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="Windows\updater.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="Windows\updater.ico" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
</Project>
|
||||
38
updater/updater.vcxproj.filters
Normal file
38
updater/updater.vcxproj.filters
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<ClCompile Include="Updater.cpp" />
|
||||
<ClCompile Include="Windows\WindowsUpdater.cpp">
|
||||
<ClCompile Include="$(SolutionDir)pcsx2-qt\VCRuntimeChecker.cpp" />
|
||||
<Filter>Windows</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="Updater.h" />
|
||||
<ClInclude Include="SZErrors.h" />
|
||||
<ClInclude Include="Windows\resource.h">
|
||||
<Filter>Windows</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="UpdaterExtractor.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Filter Include="Windows">
|
||||
<UniqueIdentifier>{bdeccfd9-a573-4076-b112-e013516c30c8}</UniqueIdentifier>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="Windows\updater.ico">
|
||||
<Filter>Windows</Filter>
|
||||
</Image>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Manifest Include="Windows\updater.manifest">
|
||||
<Filter>Windows</Filter>
|
||||
</Manifest>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="Windows\updater.rc">
|
||||
<Filter>Windows</Filter>
|
||||
</ResourceCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user