diff --git a/CMakeLists.txt b/CMakeLists.txt index 00085f4721..d1ec133da4 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 c980ab1867..bdeb0d63a6 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 0000000000..5711bbcc2c --- /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 0000000000..3910a1a760 --- /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 0000000000..087f98120e --- /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 0000000000..b697091c85 --- /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 0000000000..608e861c15 --- /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 0000000000..dbaa22b0a6 --- /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 0000000000..312731b487 --- /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 0000000000..5a7a7a5f80 Binary files /dev/null and b/updater/Windows/updater.ico differ diff --git a/updater/Windows/updater.manifest b/updater/Windows/updater.manifest new file mode 100644 index 0000000000..4258255b62 --- /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 0000000000..1cd896f3ef --- /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 0000000000..6b2ce5ba39 --- /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 0000000000..a8d3b2125d --- /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