From a289723f668c38e23519d731467365b507c7bad0 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Thu, 12 May 2022 20:23:01 +1000 Subject: [PATCH] Qt: Add update extractor source --- CMakeLists.txt | 1 + PCSX2_qt.sln | 14 + updater/CMakeLists.txt | 12 + updater/SZErrors.h | 43 ++++ updater/Updater.cpp | 392 ++++++++++++++++++++++++++++ updater/Updater.h | 66 +++++ updater/UpdaterExtractor.h | 166 ++++++++++++ updater/Windows/WindowsUpdater.cpp | 395 +++++++++++++++++++++++++++++ updater/Windows/resource.h | 16 ++ updater/Windows/updater.ico | Bin 0 -> 41545 bytes updater/Windows/updater.manifest | 22 ++ updater/Windows/updater.rc | 110 ++++++++ updater/updater.vcxproj | 84 ++++++ updater/updater.vcxproj.filters | 37 +++ 14 files changed, 1358 insertions(+) create mode 100644 updater/CMakeLists.txt create mode 100644 updater/SZErrors.h create mode 100644 updater/Updater.cpp create mode 100644 updater/Updater.h create mode 100644 updater/UpdaterExtractor.h create mode 100644 updater/Windows/WindowsUpdater.cpp create mode 100644 updater/Windows/resource.h create mode 100644 updater/Windows/updater.ico create mode 100644 updater/Windows/updater.manifest create mode 100644 updater/Windows/updater.rc create mode 100644 updater/updater.vcxproj create mode 100644 updater/updater.vcxproj.filters diff --git a/CMakeLists.txt b/CMakeLists.txt index 00085f472..d1ec133da 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,7 @@ add_subdirectory(pcsx2) if (QT_BUILD) add_subdirectory(pcsx2-qt) + add_subdirectory(updater) endif() # tests diff --git a/PCSX2_qt.sln b/PCSX2_qt.sln index c980ab186..bdeb0d63a 100644 --- a/PCSX2_qt.sln +++ b/PCSX2_qt.sln @@ -64,6 +64,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "d3d12memalloc", "3rdparty\d EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "lzma", "3rdparty\lzma\lzma.vcxproj", "{A4323327-3F2B-4271-83D9-7F9A3C66B6B2}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "updater", "updater\updater.vcxproj", "{90BBDC04-CC44-4006-B893-06A4FEA8ED47}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug AVX2|x64 = Debug AVX2|x64 @@ -398,6 +400,18 @@ Global {A4323327-3F2B-4271-83D9-7F9A3C66B6B2}.Release AVX2|x64.Build.0 = Release|x64 {A4323327-3F2B-4271-83D9-7F9A3C66B6B2}.Release|x64.ActiveCfg = Release|x64 {A4323327-3F2B-4271-83D9-7F9A3C66B6B2}.Release|x64.Build.0 = Release|x64 + {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Debug AVX2|x64.ActiveCfg = Debug|x64 + {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Debug AVX2|x64.Build.0 = Debug|x64 + {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Debug|x64.ActiveCfg = Debug|x64 + {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Debug|x64.Build.0 = Debug|x64 + {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Devel AVX2|x64.ActiveCfg = Devel|x64 + {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Devel AVX2|x64.Build.0 = Devel|x64 + {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Devel|x64.ActiveCfg = Devel|x64 + {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Devel|x64.Build.0 = Devel|x64 + {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Release AVX2|x64.ActiveCfg = Release|x64 + {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Release AVX2|x64.Build.0 = Release|x64 + {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Release|x64.ActiveCfg = Release|x64 + {90BBDC04-CC44-4006-B893-06A4FEA8ED47}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/updater/CMakeLists.txt b/updater/CMakeLists.txt new file mode 100644 index 000000000..5711bbcc2 --- /dev/null +++ b/updater/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable(updater + Updater.cpp + Updater.h +) + +target_link_libraries(updater PRIVATE common fmt::fmt lzma) + +if(WIN32) + target_sources(updater PRIVATE + Win32Update.cpp + ) +endif() diff --git a/updater/SZErrors.h b/updater/SZErrors.h new file mode 100644 index 000000000..3910a1a76 --- /dev/null +++ b/updater/SZErrors.h @@ -0,0 +1,43 @@ +/* PCSX2 - PS2 Emulator for PCs + * Copyright (C) 2002-2022 PCSX2 Dev Team + * + * PCSX2 is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * PCSX2 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 PCSX2. + * If not, see . + */ + +#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 +} diff --git a/updater/Updater.cpp b/updater/Updater.cpp new file mode 100644 index 000000000..087f98120 --- /dev/null +++ b/updater/Updater.cpp @@ -0,0 +1,392 @@ +/* PCSX2 - PS2 Emulator for PCs + * Copyright (C) 2002-2022 PCSX2 Dev Team + * + * PCSX2 is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * PCSX2 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 PCSX2. + * If not, see . + */ + +#include "Updater.h" +#include "SZErrors.h" + +#include "common/Console.h" +#include "common/FileSystem.h" +#include "common/Path.h" +#include "common/ScopedGuard.h" +#include "common/StringUtil.h" + +#include "7zAlloc.h" +#include "7zCrc.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +static constexpr size_t kInputBufSize = ((size_t)1 << 18); +static constexpr ISzAlloc g_Alloc = {SzAlloc, SzFree}; + +static std::FILE* s_file_console_stream; +static constexpr IConsoleWriter s_file_console_writer = { + [](const wxString& fmt) { // WriteRaw + auto buf = fmt.ToUTF8(); + std::fwrite(buf.data(), buf.length(), 1, s_file_console_stream); + std::fflush(s_file_console_stream); + }, + [](const wxString& fmt) { // DoWriteLn + auto buf = fmt.ToUTF8(); + std::fwrite(buf.data(), buf.length(), 1, s_file_console_stream); + std::fputc('\n', s_file_console_stream); + std::fflush(s_file_console_stream); + }, + [](ConsoleColors) { // DoSetColor + }, + [](const wxString& fmt) { // DoWriteFromStdout + auto buf = fmt.ToUTF8(); + std::fwrite(buf.data(), buf.length(), 1, s_file_console_stream); + std::fflush(s_file_console_stream); + }, + []() { // Newline + std::fputc('\n', s_file_console_stream); + std::fflush(s_file_console_stream); + }, + [](const wxString&) { // SetTitle + }}; + +static void CloseConsoleFile() +{ + if (s_file_console_stream) + std::fclose(s_file_console_stream); +} + +Updater::Updater(ProgressCallback* progress) + : m_progress(progress) +{ + progress->SetTitle("PCSX2 Update Installer"); +} + +Updater::~Updater() +{ + if (m_archive_opened) + SzArEx_Free(&m_archive, &g_Alloc); + + ISzAlloc_Free(&g_Alloc, m_look_stream.buf); + + if (m_file_opened) + File_Close(&m_archive_stream.file); +} + +void Updater::SetupLogging(ProgressCallback* progress, const std::string& destination_directory) +{ + const std::string log_path(Path::CombineStdString(destination_directory, "updater.log")); + s_file_console_stream = FileSystem::OpenCFile(log_path.c_str(), "w"); + if (!s_file_console_stream) + { + progress->DisplayFormattedModalError("Failed to open log file '%s'", log_path.c_str()); + return; + } + + Console_SetActiveHandler(s_file_console_writer); + std::atexit(CloseConsoleFile); +} + +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) +{ + FileInStream_CreateVTable(&m_archive_stream); + LookToRead2_CreateVTable(&m_look_stream, False); + CrcGenerateTable(); + + 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, StringUtil::UTF8StringToWideString(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(); +} + +bool Updater::RecursiveDeleteDirectory(const char* path) +{ +#ifdef _WIN32 + // making this safer on Win32... + std::wstring wpath(StringUtil::UTF8StringToWideString(path)); + wpath += L'\0'; + + SHFILEOPSTRUCTW op = {}; + op.wFunc = FO_DELETE; + op.pFrom = wpath.c_str(); + op.fFlags = FOF_NOCONFIRMATION; + + return (SHFileOperationW(&op) == 0 && !op.fAnyOperationsAborted); +#else + return FileSystem::DeleteDirectory(path, true); +#endif +} + +bool Updater::ParseZip() +{ + std::vector 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(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. + 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; +} + +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(m_update_paths.size())); + m_progress->SetProgressValue(0); + + 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; +} + +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(StringUtil::UTF8StringToWideString(staging_file_name).c_str(), + StringUtil::UTF8StringToWideString(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()); +} diff --git a/updater/Updater.h b/updater/Updater.h new file mode 100644 index 000000000..b697091c8 --- /dev/null +++ b/updater/Updater.h @@ -0,0 +1,66 @@ +/* PCSX2 - PS2 Emulator for PCs + * Copyright (C) 2002-2022 PCSX2 Dev Team + * + * PCSX2 is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * PCSX2 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 PCSX2. + * If not, see . + */ + +#pragma once + +#include "common/ProgressCallback.h" + +#include "7z.h" +#include "7zFile.h" + +#include +#include + +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(); + +private: + static bool RecursiveDeleteDirectory(const char* path); + + struct FileToUpdate + { + u32 file_index; + std::string destination_filename; + }; + + bool ParseZip(); + + std::string m_destination_directory; + std::string m_staging_directory; + + std::vector m_update_paths; + std::vector m_update_directories; + + ProgressCallback* m_progress; + CFileInStream m_archive_stream = {}; + CLookToRead2 m_look_stream = {}; + CSzArEx m_archive = {}; + + bool m_file_opened = false; + bool m_archive_opened = false; +}; diff --git a/updater/UpdaterExtractor.h b/updater/UpdaterExtractor.h new file mode 100644 index 000000000..608e861c1 --- /dev/null +++ b/updater/UpdaterExtractor.h @@ -0,0 +1,166 @@ +/* PCSX2 - PS2 Emulator for PCs + * Copyright (C) 2002-2022 PCSX2 Dev Team + * + * PCSX2 is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * PCSX2 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 PCSX2. + * If not, see . + */ + +#pragma once + +#include "SZErrors.h" + +#include "common/FileSystem.h" +#include "common/ScopedGuard.h" +#include "common/StringUtil.h" + +#include "fmt/core.h" + +#if defined(_WIN32) +#include "7z.h" +#include "7zAlloc.h" +#include "7zCrc.h" +#include "7zFile.h" +#endif + +#include +#include +#include + +#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, StringUtil::UTF8StringToWideString(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 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(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 +} diff --git a/updater/Windows/WindowsUpdater.cpp b/updater/Windows/WindowsUpdater.cpp new file mode 100644 index 000000000..dbaa22b0a --- /dev/null +++ b/updater/Windows/WindowsUpdater.cpp @@ -0,0 +1,395 @@ +/* PCSX2 - PS2 Emulator for PCs + * Copyright (C) 2002-2022 PCSX2 Dev Team + * + * PCSX2 is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * PCSX2 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 PCSX2. + * If not, see . + */ + +#include "Updater.h" +#include "Windows/resource.h" + +#include "common/FileSystem.h" +#include "common/Console.h" +#include "common/StringUtil.h" +#include "common/ProgressCallback.h" +#include "common/RedtapeWindows.h" + +#include +#include + +class Win32ProgressCallback final : public BaseProgressCallback +{ +public: + Win32ProgressCallback(); + + 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 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); + + HWND m_window_hwnd{}; + HWND m_text_hwnd{}; + HWND m_progress_hwnd{}; + HWND m_list_box_hwnd{}; + + int m_last_progress_percent = -1; +}; + +Win32ProgressCallback::Win32ProgressCallback() + : BaseProgressCallback() +{ + Create(); +} + +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); +} + +bool Win32ProgressCallback::Create() +{ + static const wchar_t* CLASS_NAME = L"PCSX2Win32ProgressCallbackWindow"; + static bool class_registered = false; + + if (!class_registered) + { + InitCommonControls(); + + WNDCLASSEX wc = {}; + wc.cbSize = sizeof(WNDCLASSEX); + wc.lpfnWndProc = WndProcThunk; + wc.hInstance = GetModuleHandle(nullptr); + 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.lpszClassName = CLASS_NAME; + if (!RegisterClassExW(&wc)) + { + MessageBoxW(nullptr, L"Failed to register window class", L"Error", MB_OK); + return false; + } + + class_registered = true; + } + + m_window_hwnd = + CreateWindowExW(WS_EX_CLIENTEDGE, CLASS_NAME, L"Win32ProgressCallback", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, + CW_USEDEFAULT, WINDOW_WIDTH, WINDOW_HEIGHT, nullptr, nullptr, GetModuleHandle(nullptr), this); + if (!m_window_hwnd) + { + MessageBoxW(nullptr, L"Failed to create window", L"Error", MB_OK); + return false; + } + + SetWindowLongPtr(m_window_hwnd, GWLP_USERDATA, reinterpret_cast(this)); + ShowWindow(m_window_hwnd, SW_SHOW); + PumpMessages(); + return true; +} + +void Win32ProgressCallback::Destroy() +{ + if (!m_window_hwnd) + return; + + DestroyWindow(m_window_hwnd); + m_window_hwnd = {}; + m_text_hwnd = {}; + m_progress_hwnd = {}; +} + +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((static_cast(m_progress_value) / static_cast(m_progress_range)) * 100.0f); + if (percent == m_last_progress_percent && !force) + { + PumpMessages(); + return; + } + + m_last_progress_percent = percent; + + SendMessageW(m_progress_hwnd, PBM_SETRANGE, 0, MAKELPARAM(0, m_progress_range)); + SendMessageW(m_progress_hwnd, PBM_SETPOS, static_cast(m_progress_value), 0); + SetWindowTextW(m_text_hwnd, StringUtil::UTF8StringToWideString(m_status_text).c_str()); + RedrawWindow(m_text_hwnd, nullptr, nullptr, RDW_INVALIDATE); + PumpMessages(); +} + +LRESULT CALLBACK Win32ProgressCallback::WndProcThunk(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) +{ + Win32ProgressCallback* cb; + if (msg == WM_CREATE) + { + const CREATESTRUCTW* cs = reinterpret_cast(lparam); + cb = static_cast(cs->lpCreateParams); + } + else + { + cb = reinterpret_cast(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(lparam); + HFONT default_font = reinterpret_cast(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; + } + break; + + default: + 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(StringUtil::UTF8StringToWideString(message).c_str())); + SendMessageW(m_list_box_hwnd, WM_VSCROLL, SB_BOTTOM, 0); + PumpMessages(); +} + +void Win32ProgressCallback::DisplayWarning(const char* message) +{ + Console.Warning(message); + SendMessageW(m_list_box_hwnd, LB_ADDSTRING, 0, reinterpret_cast(StringUtil::UTF8StringToWideString(message).c_str())); + SendMessageW(m_list_box_hwnd, WM_VSCROLL, SB_BOTTOM, 0); + PumpMessages(); +} + +void Win32ProgressCallback::DisplayInformation(const char* message) +{ + Console.WriteLn(message); + SendMessageW(m_list_box_hwnd, LB_ADDSTRING, 0, reinterpret_cast(StringUtil::UTF8StringToWideString(message).c_str())); + SendMessageW(m_list_box_hwnd, WM_VSCROLL, SB_BOTTOM, 0); + PumpMessages(); +} + +void Win32ProgressCallback::DisplayDebugMessage(const char* message) +{ + Console.WriteLn(message); +} + +void Win32ProgressCallback::ModalError(const char* message) +{ + PumpMessages(); + MessageBoxW(m_window_hwnd, StringUtil::UTF8StringToWideString(message).c_str(), L"Error", MB_ICONERROR | MB_OK); + PumpMessages(); +} + +bool Win32ProgressCallback::ModalConfirmation(const char* message) +{ + PumpMessages(); + bool result = MessageBoxW(m_window_hwnd, StringUtil::UTF8StringToWideString(message).c_str(), L"Confirmation", MB_ICONQUESTION | MB_YESNO) == IDYES; + PumpMessages(); + return result; +} + +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); +} + +#include "UpdaterExtractor.h" + +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nShowCmd) +{ + Win32ProgressCallback progress; + + int argc = 0; + LPWSTR* argv = CommandLineToArgvW(lpCmdLine, &argc); + if (!argv || argc <= 0) + { + progress.ModalError("Failed to parse command line."); + return 1; + } + if (argc != 4) + { + 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 Qt frontend and " + "click Help->Check for Updates."); + LocalFree(argv); + return 1; + } + + const int parent_process_id = StringUtil::FromChars(StringUtil::WideStringToUTF8String(argv[0])).value_or(0); + const std::string destination_directory = StringUtil::WideStringToUTF8String(argv[1]); + const std::string zip_path = StringUtil::WideStringToUTF8String(argv[2]); + const std::wstring program_to_launch(argv[3]); + LocalFree(argv); + + 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); + + progress.SetFormattedStatusText("Waiting for parent process %d to exit...", parent_process_id); + WaitForProcessToExit(parent_process_id); + + Updater updater(&progress); + 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 GitHub."); + return 1; + } + + updater.CleanupStagingDirectory(); + + progress.ModalInformation("Update complete."); + + progress.DisplayFormattedInformation("Launching '%s'...", + StringUtil::WideStringToUTF8String(program_to_launch).c_str()); + ShellExecuteW(nullptr, L"open", program_to_launch.c_str(), nullptr, nullptr, SW_SHOWNORMAL); + return 0; +} diff --git a/updater/Windows/resource.h b/updater/Windows/resource.h new file mode 100644 index 000000000..312731b48 --- /dev/null +++ b/updater/Windows/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by duckstation-qt.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 diff --git a/updater/Windows/updater.ico b/updater/Windows/updater.ico new file mode 100644 index 0000000000000000000000000000000000000000..5a7a7a5f80bcc9d7e054042a4057dcaa45a9d38c GIT binary patch literal 41545 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•pIa+3T(}|DGTiP^U zlIy8PLvwU|Nklz>J6DI7+=+CCP*`8XHrz&t;i zmWodbhzJp4hn0?$3Jak}#H;9TE{4Lsp*{~VYSOiP{_DTVmxpqXmWArBx8%^>QA1xx3sa- z)l8#)+16CU3znyGvA>9sPRmJRXx&KFHhzi4pj-qhuzu#B=TUF#g9a~E#dff4Lx=R?b+2=yw*nnU5?$I;U@N#mP4e#TC^! zW2t6{zIZ5dw2+y@bo)LP;JMc%4~yI$C#{s!gJe}YQnBke{ovA-pVw48X73eQI~@z| zeZ3hc*k7elOX<~+NaMIEF!P$3*F;E#z(4hz^(Ut_0lqsF;Zv5U^;s17l2h{k#z8NV zDcB6I+9yB#abDdzfgTVX^?sWI$iwGl=Qo?xFRX<4$Y$ zBp%Aw-ZZ|F6xx8dG4tp1!!a?*m6<{JLRQ(2pLFhtqujP1fxiq(5Lhr+q5%q84y?a1 zZNe5zL*o#7)U_v{fCm_9rq#RhxQch3aecwq+>i`3DrDULB*)y|x_Uk|c7kC6G^{WY zsN7$<3h$G|zF;ZFjhita6h;YQ5#dkvlt{uC;CZc+-hpS>`DF;!<4Nr(u8FTmXt4rr zfMO!6w)4TrSC(t(kIg0SU7Z5S{O8IW!65$@R?V;A2p{a0jRTHpGXvB@UAhHP~?(i%>73n=dX_676fZb-w zuCw=~Lwsv-_lP*+l?=;R#B+9cd2~0tEa|+jkIla~9ksGEVb;SfDzkhjwnXcvI1XnQ zwf-~8mlj(6#|6Vdu?tR~yoG;AKO&n1d08So$A%W;gDzxUi!BzkK8kRz7=bEn=>n-yYy4uOP$JqhkHxZ>AJkUo-VT_!^~ znL0Ac#TsUWaSj(8m!8-C)74M(_Jon_@%63LG_=3(JYqbg&8e%6E<;Ry#!I9SVzhml zR!#C4(3GSus0sNO5IbT_`~1yvTorA!V;LW00XI@!UKlT(oQZ}0>Xd_!bpG{=f}GW~ zV`}{O=}iQ+9KdkQ9`$^LM;fWw`}6)5Mo-GKf%ol!XbrMKoO6HHRPHpr%fdej`Vu?O zSbX!Z9o9fma&W*6c)fPvYcOuM<}iy^Ww>+yhkrVB8sKi!^!w8n5GL|Z+@j*V-q1tg z%_rNGsPBj(VSC!$`wT!D;Ih3~N{t|+vE%D{a)n1$Wet^o9_OX!Tsg+C0ilR6eY(CY z`iap@etJEgv&gpvi5}0G6=TdopN*%apV=C?t<4;`&NFh|vcG#E zm1a`%^|$zz_FDf_Q`~K@kWC#Zg5X9qN9hOOW!vM1WhQS8Nsy6B4hzdbi`3S1$+QkM z8$E)c>_PSK#{o+{WgEC#XX6ll^_Bjx_7Qi(JJQ!40fwLG+Hf?XXOtMW8h-IgL0gx2 zH5jx;wz(&svh+pN=Y1jL`QpD(4-RSF1*g_i3mKlsV`#qGfu~&IU+IFLYQBa0p)jWJ zPgbCVMd6$5sIZ{_I9uR%hoK8Mm^MGo^>BmU+TB-t8=oCqEjB1%r>j(01XarxSyu4+ z#ha@$?xJ%JoFRmYBdiMrC7=A?noGTpC{FI%3lU62R|gLG2#z^^!;Lclir*(>uLcyMZg~ zB$JnaA}%kV8?Y^FX%Y$evA;z4oySPzJXkeaO4TWx*}#1n8(wxiZHR##IA2-R;>tb= zSID4Oq<8+m98TN{Lo5YxpNjpC!@v;z^pE_{Q)mIq ztj2sO8v*M$c|t!OU&DmT`}o#_8uhKpmIVDEB++pRp;BCr{V~D3 z?Xu&vR6tw^s14l+lnQ_!q+LNDui|SGP&sSHNM^}s)OSHcbHbnhe)lp%mdn+LGcMeGw(Xk8i_7s_9b>oSP>)SJK+b$cyT*1mNuWJKf2u1 zcLIwQ8>b{8T)08aOnIy&Ou+D6|7O+$lKRHKB&P)dJ0X``S9z2OIp-WHw<%zO-o~7= zVfG*zlrE>{Klt!*i!hRx=~j+=)N~JLCLIv+#rOJ?91hrIqNZKiRJ-IgF*(k&`%jmK!i_{{2F=>T4%so0Dd?(q$~NtBUL~x+-5S#2d$7npnn>?G6AyH> zW=Mdg*oNa~T8+rufZJfL+iN#uKv)<%btP0F0b%2>pqVuA6$-iKdcUcEvAM38DcCHy z+E(P3k2r3LCf@t1bnY0u-mi@~J;&R6>YLF}t^J-opYLuFK}W!vl7{XVBW+P9Zl{@f z-cj~&(?Isu0L|Yy9W1leWQrSU?(f_dmF#YhZm#(RgYjfR+!PSH~tMi8bS2=$kfBNuVlC{V|tg1aT>G$5C&X}t4L62`*%`Wqo_pzMUe3;B!hPLCt}aQ zHFqzki>{#3wGt66E{ox9T@h{7+{w(jITD6kOTzagni6vQ*EM+5Z?~R$FK$4+W&4P} z*Sd9qr-qF4fw<%GnGj8(>W8$#-;p1=OrMWzZ$xNH*0zuoOc|b>SR$U^u(^MgLH-}5 zi;t88XIGpkn$s8GTxDTuFe3NoyAm)vHn0GO^Dg?F_CQCfw^mfH%&?oh zMBvR>s(7-mF7lmB`(dEH%><{}x2QXg4zt_o56To*W@vE0L!kb|>GeEy=~4AtqUJc4 zMMmVWhz+*IWQFH!G}7gAqv`bro$e!4xjJFaKVdS0Z&>FPhur-_>!41@CLmG6QVj6^ z7#e=ZcXA!o8`63Fd1k6ah5|Q0v;D=u^prI()0bua$Pmx6)EIy3k)y%TaKeerbDuPQ zID|6mT?X!?YAL{9F1=M>;D!t3(r9>39LCuc1M6^8mN?YpaG>4Y>{cmm@|Jduww_bwo+Q>c^o+?{?o?dw-qjacqnLlW;_sq5Ff zLNfOAD5@O%>xyngBWI3B*R$ z#Pd*T@*^XoYUWch6(CT&LGUAWJXK!9@nTq$IKa680me(um(xlQ%!itkulr)}GSPNo zmwv=f9z{trMNWaz-O7iD4%mPGcZDj%cJ^`eT#LAUY1~U?JGSxdz#HZv(bD9>!RG+vD=TsWl0SwAg__LKe(#Xo zzx@lJ9r9)@E%AINf2*ARTbK;J4t{%&OLpL_c;Am#i)&p9l>Cn_?kxh$zg?6CoSUAX zhwTK<;71nV!_wNXuW)B@fmq4l;hs=phT$wQAA@Eq>SyOa`U|_-RgdiHPGWT1&vYzK zg-%)ZHithGzSF;cZA|zI|K*}U70d7FS`MaY#rc+0L`8?v_38~ui>fvxr4J0k39 zyrc8*Az7%FgBQW!BZPsd{y~OS-0R}m?*X7x2+LCenUQXZY{9Voc!B)!mQu_7Pv-K8 zS}5JgXmcRQGjnHlJ38y=%*Sq32sOg|H|)J%VR)fB#N9Ec1C6FAHnr8j?G zL;v8QnntqOyxJMhEe8P80NsWm$Hn}mvXst0mQ^~h_^4DOnyK^t(K94C>U>^^%XY6k znT$)Xk=aSd%iT1Mbf<*+xDuMgwg#156?F&NAKG|j(#6pg!H%;TUv{u4;#*6H`HpzU z+p35*rP^CxkQA&j2_gki3Q89j-r~jKas2MnoiP(b+pN0ecHfGDwbxoqlBurxq1(eU z+?fymsnJF^Vo*EcjUxZ)QI*C6To|U?x_gc1h>C=HZi!`|)VXVAOTk;YA?YL`)0)+o;9!i z?uB{PUPBd6&n_X;8TAFNS=;6Jx=C5MUf9MUDapQq#2R~CY0gpxN3Z3S4xcAd3i&;| zp9?q`e3H+b;#@}4>WqlhfMoEMR~kb^lKb@z4(eVuhXeuT(%CYRB};eoS=I9pqL;rY z+Y7wi8LN1IR)1->u)Ux1`%Rl1I=3=5y*+e-P&bO39_Wt(-VDFk*zA)~AhKpdZhnnz zM%L%be0g4np3k2i7rUM1z0nepa=U*`=qGk6RhA;|?7Yr){Rt*L*mXTi*y}kLX~xPh zwz-g0EM4~iRbi>=; zJ7=45y}d1q#)89!yqy)o%}c8KYfhvHY`)UK9e}o8>(8&yELOQKy2b3k*QcDD(81!I z8xblG-c`PCD}BFFras0q#qt6tc2;PDMbx@3qcDm?$_{_IP&IqYq2&jBf~j9!&*<^5 zaMDD!6dogTiNO0r$6MZ_r@2x>MwTSAJky$-?PvJ;wK5 zy;x$jh=~F}mk|&0t}WiZKcvY8 zroODqnbT6$dQm{i*sFBD*@vQ~j*qo6FFQPO)D{+^%1z&}ckkI$L57-S?cDtW_Y>-5 zUaV%n6T=lR6(_$+X{ErIU=o`VgLS7dMf;_!5%pPJMxhAvt+%-$V`+PRC}xc7wMxFw zqGp+GFv%(!>{U2-Qn-2eGhMjT;Gp4wB*i-MU5cDaQzqe50lgVRN@c=EY z;FSU8J zJ$-X$r%C-z&YU9AW6f%NZR+3e`tT*IhStlvSF$=HR83|1LB1*bFLx}qxR^Au<#NIv z9`!D9+4+lU&$mPqmcq8_I_gEAk5uI}9<3CDswn8RZFl167mghW>tv@oyw<63*WFh?Kb-O|ojY;a85IZRu3G~)Us3v=u;#k!aULt% z9W&2!DtnXB^5+L1d~JERI^XF)X7cj773zX8p4VXWz?8vj zc{$JCVN)i1cDHM5T3E!_YcZ}(-R~#N>vp1OL=;o*ev`N-U|FTTqMh(mLD9@CmYqDz z=Av&n&js(8RdUs1l>JByaTmSH65(;ZYfsL~=32Fy`E{;qSwq9LL2qx{#Xd&+gcq9) z*N220410g1i@R(A6D7A(qNyZ)U%je{=U2Ut?>hX>={h)XaShtI`~7J4-rW6`nPk(|Qe6eL`|xCmt`T3^{X@aas73;leBi-p&_UpTm+?8A44P>vLOj zJ209t^Loq0E#WtNw^}&Xr7hgmotYV;2d5Hnls_NqPZiuSn?cfUfOpqA$I{uc#*!x4 zRkaRxx!4}WuD7~we4L#&uwUXvOx8))hcgsr$WE!)Z$xU9d#`!wL{71n#?IEV+{_mq zCFf%13KlTcG02s@U8B66<+0>eS&s5~tTT%meBvKfo?N2)(RP*QwcD(lpZkhvPk+Nt zl)9UDx_Fd-<>4rAyw3iFe}e|S&r{kd?U%Q&enB|J$j;nZx!r`V=4`u2osGVM*42}< zeR)gvcZc36+`>TG>9(=&@XM9kSWU0<`CpyiVUjxk?2Y;D>uB!A#mu+-H#)Vwy_X>4 zcD-^_3ME@pV_j&eis<5!{c{u(#ww%ZY%RJ8mcb-;E zHRRp=nsS<#g`W`YeSU7G=Pan1Z*+3mnLGEErpKBEGV0|~Ui(-{?q~mUaa~PWMSpUs zsFArug7A)zxR&4+7RUWO;{dA6>S6#Z_bD24f?_XU?R_-2KMbmq; zwUVqEn@F3Zp=K1~xs5((B{(|oez0U;y&C-1*zMAqF5NFLA4n-$=x*O6`)=AmIqEq4 zWNmri9M**;(K8nakhYc_pFyqXaBGS<`N{ce+4=?Rs)9P(L_oXro8ec~J)y_Hb^UBATSoFA3_At!2(DN)-a-ff=tC9TV z_THm|i(gcG=Q->Ms5o+)WNf3n2B#Bx@%D}whK%h}5Jt#V%1Y8mU>Ysp@GzFcTd z?^kn0{q<`ty;4^jI=@KDcu`DU_E~~Iv?Zi+Z_RF#f>%3R&|sM4Q89}!C+!;57gxN* zywxt2H^u3)6r?|y8lYn{imUp8@v2^w9jEA5X;9u#HcNNc+xznG9hVF0sXtbi+F4ri zRGwW>TVVOijN6|B@35L(vt~2cu68R;^;7#X$Ey!S?AHzOOY>JOlTA!HA1x=Gx3X|e z?e@FQY;3!xTz%*?O*_cwVUvjBZJNf%#b;8=OD%e}%cjM-r9B)m(!V7#)|MWSyXT=@ zOz8G1ZXt(dhb;%K9@zUXm(qtGsLa=4{xi?Av&9>vb|r^p4&~kn(dU<}3|WNCm>lNc zaSz`yTRgz6MCEJst4b@S=Zq=}M+#QwmbDz(7=5|-_SCqf)D5tJSienqa8K2m24-m~ zLB%y3M^DADIXa%urWERWCD)P_b<1CVydaj+uKqx{s?EqMEp?%NvBS1$-p}4Xh-27( z#zalcZ79T>)0 zO%3S!f?C&(t+GCp!F%cQxx+U+?Kd?)Y+k%`@D=Y#mo-up?j}{)G~O)By;(lT9#pQ9 z@u;Z1Y$fTNq&Gan6s_9Wsx{DbSuA9ZWGAl}B zoHeYJDc{$j`C-Rq8KS|lv*CQYnd!;MONO^C3@;0@G{%RYtkHFFQ0^1yo>I2*VtU(s z&4Sf;&vF&Ku`tWObLURwR*A1i84u3Qz8BxACqTYtIq18q`05w#>8J5r-~p9E)ni?# z5&Zt@29>EzYG1}P_1Qfe8y#zpZ0c_Kva)NHSx$~_Oigt|;*0ppORWP3m+@C@I4SXI zk5yb66U$+xf-Y^oH#?tN%d%PoBbzM|{2GOub{k&v5~NbDd*>sZyt^zeeOug%GzY={^*pxPWiI7k-T6^!7Z6x z2UK!)Rj9>krO=kDy^thJF6l0C%_`r_zT+tKUXdbdu+~WWiAzoV;vVv!=B_v-q2o=G z;BjHLRjOXoS?BX<3+%WBidp2`t;I9Iv?ENpWcEaA+SfaryGh1E417@xL-Ix>Ml<*K zUmrQzubUI(HneSrmYIp|43QC)=?(XZM4EOX`-w=eIDg;8?eV+Cg(7mS#aK=a^ZEPx zKXqykO7c5;P+fPa{+sJwVz;@RSq66299=D6N-R?ty>rbbvX9DTK{`^)P=CXH8cmAB zTe!V@_Z0SHH>fw`Ja>u_Dz78>oNTHC54oR}8{1O1U+X%v+B$P0^{RKju2g-nUKrEn zQ!ed+%tIVPhu<~J=@yCd9F*2{Rr$>3rW~lxxItq-$8DY+htmn4KbZQUT@8^YvIMxaqmOdqf&Nv z%XEg)vUUfiM8<2k#Y7|2H6LjfKF`>{uyv)&Cy$Dtc=dE@x>MCgr&b}d<0fv~dp5@z z7#NbeXuR+07rm){D>-YyQ0mj*gT-$Cmn8OC?0faa+pRKNA^=@1XnPLlg=ws~;9XNJ zvVYJ!@Z7l@6$xT!wb)jP@X(< zjZp2saMFEIlU-cUc3UX{tmx$7@=`xegN zE>7sZxsG~ImetvVRueX4b88BPpSEc2>%AX0%cPxXajDuePit1mP&=NHLJ`}>laGnPYW8+oMT()^=Sq-i|mq_MSjAc7>HL&+Zo3WN4t2ef4yw}D{qoYv~J6n8@<`KaU|oW zxqL0mc^m_0rVAa}!V?(QEU$OT!>Pdf&Pc`i+sp1$lq9W(Rcv1Hl1m0MmNeCfaqk=t zrLAnv?N3=lJd7z`uwbVSx4*xuXz)w8RlgHP>vyoLOr1hCX9_&CW9E^b zB3(NJvhy=9jm`I@HD*S+u3NNrz1)!EsAFG?{_cz!ZLn7mRKb!M+Ez5QoKLrg=XH`< zi}~=zj4SMoOWu_37FpKkLSB@8PoqD|uPdAF4C)-;KQ% zYBfWv((s~GsQ8=ky|0X;bh?sgU1pICDOON-z&rdzgLi@8nHM`7#iS(_R2Os%s(*NBze+`7 zh&6HLrF{>ZBs?^RUxcm;sh2h6PYg2H{9@2=)0SX^6=l}y)3h9=H?!BAm)jol?&;;^ zw6DcSTEghuM}J9a(Ha1ns-JyMlImncM+orJ`L!6&%&s?_Nt7g6PZRc5K zA7}YvCnF2rP8o$dwAB-Km>u8{9KF1#drX=Z=2~-P&zU`Aapv_I6WWyYbPji>&J|AX z)hluucCN0# zSFM`TG2EOSv%cIM4mA=;V-A%#7;9PlrPVc%N%ZM~{e0F3^eeZ88YVDxMRV_s6HDc~ zciGYveypAo-L~n;ttG+_pFbBqz%5Sh7b(xUbi`#$HgnBYwom3G70|@OXG^{8zhkIc z>az%rY+}SeOuk}Ib9NXw{2(;Z<-tSRmcu>{#X9yiX7Te@U860lIijD+bzj@wl|j}| zb5K|HOXT{g{)EfR6vjOrPpr;ydK`BN+x^7rNoo}@XZbE0L%c%K$#2^)0H=___Sy-Z zTz=j_F}3QKlozwj65kw@DCxW1JW6?G=h(6%ugQUhP~TTa-7)(NdmG=StlP<#R?f<} zX32RpM1;{#*Uu#{YHs@D&Hj(HMad%154E(`6?*Q`eYbVr7(Q2wm%*4S8z)yctUl)a zjO9dBHhWMbe6OW*vfRgsJkX>+%zpCmp^fO+V(32S+61=h`>-8V=-4-VWtrfqIF0Gj zc~12H}?vkwmf-!EH7PriOiJH8()Oz!GY^eO_;S^|SX)JQC6Dkru6QoA>q~rQ zvw8Z>5rx5YEoqfBRvD5K^qU5Y`1ZYZzy7e?BXp)TZ^0LJ)+!n6>`SHIYhG$BGv9OK z#fK-?-x=1Y%vzbgkXb0Px^i9o`b3+9wW5ZB;#x^&srv?BOCEe3pt|+ln0CjzuX9)L zX3k&lyCA=9fPWgux5ebfdx{(!Xcm&4IaBvK!%&h_;MWx$K{NOg)b1Z3?j!T6qiQ-8ZaQFT z#^)4I;^rl}*5M_aQ&7iNk>FF>r%mMqI~N<79%J6LQIv^=*-xoeccAMPwV|QKKP-Y* ze11*fL}@$08I@A7e|fWMuIGmcmwgiUzRoY*IU*G84fiG_9Q0&(YzmX&QNwHVGiI1T zZ=IdU?AhlrPk=Q=Hd%7efb@O}w?b`|?tlz~^5EcdFRS^D{m{G!K@uF3$aI>*&KG3I zaeS*N*Y;iQuM%%fOY1veqBO!E8rf~55Oz(wHD=a^2ZutLKCk*9es+~syR3TkW;v=s z7)i0}9MynwgC&gQEy_-9AFeL2wK0%o8G!Ru1N>w|g>~*m2EC{c?v+XF?^Bo@ck6Xo$X3i;Ymz{= z97MgF52}{1$WX0GrzuP)K1#$l6J!zVL-|@yw`+57#d8X*txiq#QloIlo(j6t&vq?o z;W_DxZ_>&Bho{UF37EYohCf`5Bsk2ZO6HrgA_smA{o#48N`9M#7r1pMj8L#e651%X zBBgFgZD#XkVID)Rq8XBVhr~2vy&JR)`t3xG6Q}v?&>K<;JHq5(iEh2ImnF$0Bqs$c zGbz{dQIn-Cw~x-f7uWGXl6x8H(lLk1OUFulDI1%!s8usF^y9=;E0rXt$?hBRV(E#w zUUBYaaEDNtjq55ShAEnlkjs3zXZ-SH5IT73Btx>w21$}TbH&!V{1jV3ADi7gTnTWP zr8UcG>T7~L%T?0C&!hHwmnItz_wMCgg{;x>Ds!LmiZ-e_UwU~Vz~pJ4>I?yGJx7V- z{t~g7WCOqIXo?o%eAu*Py3W@3Zuhfwrb6LxOGYyUHMC2F*0D%?2p%|;j_9)Uq{yt$qI;crINva z`V{HhMxCZKZg@k1{Vc2Ek@hPmT^=^NzCA4ML*f6db#*u8e3Sv#HeopObeezG2rH!l zr)FZ=dNg>f+;23Xzofb6nA1CRS-(}u8bgMykNl;ws8c924087(5^NteXe;O-2j?f- zCs zr`_VLZEU?8Om)Yf~ zNr*Ok^1zd1jDg3*Fb($zlS9+q3B-|(uFkPKj^C|gbrH=1D zVIiMqT%E5poy>xmR&cR5Eq5tqr7Pp{la&8@uZZS?jgr} zZtJypI8&~%T88n)XMge@)iYyPvUASf1D)y@Q6r|V<~v-W2ckq-$$m+-hiKjkYg*N8 zEE+wkZ&@95s6JK1Cp;n=A9Tw5z4~DbJ{OXrk)Z}OmNO(HhJ7Qf?G=`?Z%LmU_RB@& z)KzqriJFVr@z%8L5%}6^a$}cbwV>@xd}gusG;*0N_~NCG1E)v%`eI9a=h}^FO{Z=U zhTVt1wz`e%W!bS|$t^j>VuAO{l}n6M2Fq8LKIH1eXFwlm%H3F~yAe^DHQoGosQ2g> zu5nHDjA`Wy(~p<*-h*bZTYQD{Ysa8axqY}ty{lhSj!tz%#b|%IEd2y%-FI zYj2Py32tcWHd(wU=Q9O}$r&-1Gb`+Y<>Be4;bYDbfZ#Np_>#61nnJ&v)=$ zwcXQ0UJNC{tm$Ly_wA7h%QBMEtWc)Y1H92vu!4t|3`YB#3-J>e%av3@ncBbd(u|Er z##`X9uwpL8b$k_4nlqAVI_4c$>E|VH@Q`GO=5~yUqE9tYW1>7h39B8E9crBmoX&s(Jfe z*ZsHl@KaPO*HLPyrcEdlixm4)eTKV%Ob&VsB}^B3b|3BP9;tphPe%Wq-xLbV(Mow` zzG~_n6|z;os>1)wS>iuF-h5C;m}#xp(BQ^MIEl^&;cE|Y(Q52*(0l6mg=b)UJlPE7K z94SbNBd{S5fd3cGe?7=g;a~v78p(^HXGh&oJNyHMwn*=v5OpZG{frNXu7dP*T?LsM zJMBg2RaGt`KGxFl;lWj;rYMg%S(sx1jQ?!|6sB-!Sk03`F9Y3BOW6JictQ*~?HT?# z`~aigEEz>W(>9A`sQqCnBAV|144sd!BmG75u?-mj*8iprA=dhvUIp$%%|W}r!=AAZ z!F%ZS8h9_&pCv=1Zn8rI^_7Ttb?1lZe^QRDmTDtTR+dsuR%WjMqzy|XxCB*ZaO4Sc zFkLG@WqA|E-CnKU>xTFdTPqCmMTRg@}e5KgV&~#o_#K*?im0)jrqA_6mKx4p;j#o!rwI=l%%~R+o zK7AMw^%diIT8@Zu)Wbak;%UV?;z@Zo@#H$1#8};RElZl5{8i)s15b>bo8;ZmH5g!NJC@(wPuE{*a{bEs~rl^j%SA@RDd0!92 z&&5$XneJbWfjh-uCSUi>Bg+wyy9KZn5tlKXF)kB%+ZYXI%gU+Ch}TT#<-R+GgKZYp z1)Qc5_X^92>Ow?R7koq3QBO3Dj|YJbQ2_YoWRUi5J}+@6uavl*hsJRk;>+ktFtyKX z%?f%NobIm%;BIC-i+|wp2{V|3Z9+sA#%6>tvYnz(&K5>(c`5d#b7r<$EmlFY(}fT} zCwn)*66->D^O=E91m;5A!hpUXP!o3&$%#xueC%w+U;__MOWe$1A#P+R5mniUsLDb_ zC7>#+xAk%Y?<{^!XHF)Dn8Lw+1OQWi;C3T>GjJKCbHcnTGtv8uXrwtu4%eqTfH*x}MFyED z&+sFzrxSqsvSXh5DpR=&m8Wte9u~$CHbw@=Nxgp*7rJXodixg0VwJwt8=OMs`-IM8l6l+RwNh}cI zWF*e776G&Y5Rts*8`drshdzfHqX;k+UBASLuB8P52mnH5<0uLAMAE_n*oIetxmefm zm5ZV+X;Ghvt0{<%D;LpdaWXn+XO0B9IOyv$;LJ3CD)$k<*b)(b6lA7>u5PoWBi&&+ znj_4Q*cllD!H%EJnbbr%8KAz$0MKk9_DAx9Y>Gsp?F_(G;+!pl7+{Bn&)TBHmWoK2 zn;Ahld;sWRZs=;t3gXJeE&xIR;>rc|CNTtQ&6$a9=#v!^IB7IjuE2JYG9rr3BLaBS zV|-ndgkHs;qyT4$`9j<`0chncF_gC65S47TK*d|k=_rCY%fT3_iB82fL}R^^VWBAJ z@+}%7Xs3=inkB@J6sEEbfDP_GCUT6#S({ye0RSS-uA-x29|bAS;KDYH!WeL9Dkn1o zaFPN}HE4np=GC7Qhn$TlXr{0rGF6_1ihXuL|C>0D{BwxNKSuz
mW0q;(=X*_7J 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 literal 0 HcmV?d00001 diff --git a/updater/Windows/updater.manifest b/updater/Windows/updater.manifest new file mode 100644 index 000000000..4258255b6 --- /dev/null +++ b/updater/Windows/updater.manifest @@ -0,0 +1,22 @@ + + + +PCSX2 Updater + + + + + + \ No newline at end of file diff --git a/updater/Windows/updater.rc b/updater/Windows/updater.rc new file mode 100644 index 000000000..1cd896f3e --- /dev/null +++ b/updater/Windows/updater.rc @@ -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 + diff --git a/updater/updater.vcxproj b/updater/updater.vcxproj new file mode 100644 index 000000000..6b2ce5ba3 --- /dev/null +++ b/updater/updater.vcxproj @@ -0,0 +1,84 @@ + + + + + + {90BBDC04-CC44-4006-B893-06A4FEA8ED47} + + + + Application + Unicode + $(DefaultPlatformToolset) + true + true + false + + + + + + + + + + + + + + + AllRules.ruleset + updater$(BuildString) + + + + $(SolutionDir)3rdparty\lzma\include;%(AdditionalIncludeDirectories) + $(ProjectDir);%(AdditionalIncludeDirectories) + Async + NotUsing + NoExtensions + WIN32_LEAN_AND_MEAN;%(PreprocessorDefinitions) + NotSet + false + true + true + /Zc:__cplusplus /Zo /utf-8%(AdditionalOptions) + + + Windows + Yes + + + + + {449ad25e-424a-4714-babc-68706cdcc33b} + + + {a4323327-3f2b-4271-83d9-7f9a3c66b6b2} + + + {4639972e-424e-4e13-8b07-ca403c481346} + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/updater/updater.vcxproj.filters b/updater/updater.vcxproj.filters new file mode 100644 index 000000000..a8d3b2125 --- /dev/null +++ b/updater/updater.vcxproj.filters @@ -0,0 +1,37 @@ + + + + + + Windows + + + + + + + Windows + + + + + + {bdeccfd9-a573-4076-b112-e013516c30c8} + + + + + Windows + + + + + Windows + + + + + Windows + + + \ No newline at end of file