updater.cpp (13596B)
1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> 2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) 3 4 #include "updater.h" 5 6 #include "common/error.h" 7 #include "common/file_system.h" 8 #include "common/log.h" 9 #include "common/minizip_helpers.h" 10 #include "common/path.h" 11 #include "common/progress_callback.h" 12 #include "common/string_util.h" 13 14 #include <algorithm> 15 #include <cstdio> 16 #include <cstring> 17 #include <memory> 18 #include <set> 19 #include <string> 20 #include <vector> 21 22 #ifdef _WIN32 23 #include "common/windows_headers.h" 24 #include <Shobjidl.h> 25 #include <shellapi.h> 26 #include <wrl/client.h> 27 #else 28 #include <sys/stat.h> 29 #endif 30 31 #ifdef __APPLE__ 32 #include "common/cocoa_tools.h" 33 #endif 34 35 Updater::Updater(ProgressCallback* progress) : m_progress(progress) 36 { 37 progress->SetTitle("DuckStation Update Installer"); 38 } 39 40 Updater::~Updater() 41 { 42 CloseUpdateZip(); 43 } 44 45 bool Updater::Initialize(std::string staging_directory, std::string destination_directory) 46 { 47 m_staging_directory = std::move(staging_directory); 48 m_destination_directory = std::move(destination_directory); 49 m_progress->FormatInformation("Destination directory: '{}'", m_destination_directory); 50 m_progress->FormatInformation("Staging directory: '{}'", m_staging_directory); 51 return true; 52 } 53 54 bool Updater::OpenUpdateZip(const char* path) 55 { 56 m_zf = MinizipHelpers::OpenUnzFile(path); 57 if (!m_zf) 58 return false; 59 60 m_zip_path = path; 61 62 m_progress->SetStatusText("Parsing update zip..."); 63 return ParseZip(); 64 } 65 66 void Updater::CloseUpdateZip() 67 { 68 if (m_zf) 69 { 70 unzClose(m_zf); 71 m_zf = nullptr; 72 } 73 } 74 75 void Updater::RemoveUpdateZip() 76 { 77 if (m_zip_path.empty()) 78 return; 79 80 CloseUpdateZip(); 81 82 if (!FileSystem::DeleteFile(m_zip_path.c_str())) 83 m_progress->FormatError("Failed to remove update zip '{}'", m_zip_path); 84 } 85 86 bool Updater::RecursiveDeleteDirectory(const char* path, bool remove_dir) 87 { 88 #ifdef _WIN32 89 if (!remove_dir) 90 return false; 91 92 Microsoft::WRL::ComPtr<IFileOperation> fo; 93 HRESULT hr = CoCreateInstance(CLSID_FileOperation, NULL, CLSCTX_ALL, IID_PPV_ARGS(fo.ReleaseAndGetAddressOf())); 94 if (FAILED(hr)) 95 { 96 m_progress->FormatError("CoCreateInstance() for IFileOperation failed: {}", 97 Error::CreateHResult(hr).GetDescription()); 98 return false; 99 } 100 101 Microsoft::WRL::ComPtr<IShellItem> item; 102 hr = SHCreateItemFromParsingName(StringUtil::UTF8StringToWideString(path).c_str(), NULL, 103 IID_PPV_ARGS(item.ReleaseAndGetAddressOf())); 104 if (FAILED(hr)) 105 { 106 m_progress->FormatError("SHCreateItemFromParsingName() for delete failed: {}", 107 Error::CreateHResult(hr).GetDescription()); 108 return false; 109 } 110 111 hr = fo->SetOperationFlags(FOF_NOCONFIRMATION | FOF_SILENT); 112 if (FAILED(hr)) 113 { 114 m_progress->FormatWarning("IFileOperation::SetOperationFlags() failed: {}", 115 Error::CreateHResult(hr).GetDescription()); 116 } 117 118 hr = fo->DeleteItem(item.Get(), nullptr); 119 if (FAILED(hr)) 120 { 121 m_progress->FormatError("IFileOperation::DeleteItem() failed: {}", Error::CreateHResult(hr).GetDescription()); 122 return false; 123 } 124 125 item.Reset(); 126 hr = fo->PerformOperations(); 127 if (FAILED(hr)) 128 { 129 m_progress->FormatError("IFileOperation::PerformOperations() failed: {}", 130 Error::CreateHResult(hr).GetDescription()); 131 return false; 132 } 133 134 return true; 135 #else 136 FileSystem::FindResultsArray results; 137 if (FileSystem::FindFiles(path, "*", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_HIDDEN_FILES, 138 &results)) 139 { 140 for (const FILESYSTEM_FIND_DATA& fd : results) 141 { 142 if (fd.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) 143 { 144 if (!RecursiveDeleteDirectory(fd.FileName.c_str(), true)) 145 return false; 146 } 147 else 148 { 149 m_progress->FormatInformation("Removing directory '{}'.", fd.FileName); 150 if (!FileSystem::DeleteFile(fd.FileName.c_str())) 151 return false; 152 } 153 } 154 } 155 156 if (!remove_dir) 157 return true; 158 159 m_progress->FormatInformation("Removing directory '{}'.", path); 160 return FileSystem::DeleteDirectory(path); 161 #endif 162 } 163 164 bool Updater::ParseZip() 165 { 166 if (unzGoToFirstFile(m_zf) != UNZ_OK) 167 { 168 m_progress->ModalError("unzGoToFirstFile() failed"); 169 return {}; 170 } 171 172 for (;;) 173 { 174 char zip_filename_buffer[256]; 175 unz_file_info64 file_info; 176 if (unzGetCurrentFileInfo64(m_zf, &file_info, zip_filename_buffer, sizeof(zip_filename_buffer), nullptr, 0, nullptr, 177 0) != UNZ_OK) 178 { 179 m_progress->ModalError("unzGetCurrentFileInfo64() failed"); 180 return false; 181 } 182 183 FileToUpdate entry; 184 entry.original_zip_filename = zip_filename_buffer; 185 186 // replace forward slashes with backslashes 187 size_t len = std::strlen(zip_filename_buffer); 188 for (size_t i = 0; i < len; i++) 189 { 190 if (zip_filename_buffer[i] == '/' || zip_filename_buffer[i] == '\\') 191 zip_filename_buffer[i] = FS_OSPATH_SEPARATOR_CHARACTER; 192 } 193 194 // should never have a leading slash. just in case. 195 while (zip_filename_buffer[0] == FS_OSPATH_SEPARATOR_CHARACTER) 196 std::memmove(&zip_filename_buffer[1], &zip_filename_buffer[0], --len); 197 198 #ifdef _WIN32 199 entry.file_mode = 0; 200 #else 201 // Preserve permissions on Unix. 202 static constexpr u32 PERMISSION_MASK = (S_IRWXO | S_IRWXG | S_IRWXU); 203 entry.file_mode = 204 ((file_info.external_fa >> 16) & 0x01FFu) & PERMISSION_MASK; // https://stackoverflow.com/a/28753385 205 #endif 206 207 // skip directories (we sort them out later) 208 if (len > 0 && zip_filename_buffer[len - 1] != FS_OSPATH_SEPARATOR_CHARACTER) 209 { 210 bool process_file = true; 211 const char* filename_to_add = zip_filename_buffer; 212 #ifdef _WIN32 213 // skip updater itself, since it was already pre-extracted. 214 process_file = process_file && (StringUtil::Strcasecmp(zip_filename_buffer, "updater.exe") != 0); 215 #elif defined(__APPLE__) 216 // on MacOS, we want to remove the DuckStation.app prefix. 217 static constexpr const char* PREFIX_PATH = "DuckStation.app/"; 218 const size_t prefix_length = std::strlen(PREFIX_PATH); 219 process_file = process_file && (std::strncmp(zip_filename_buffer, PREFIX_PATH, prefix_length) == 0); 220 filename_to_add += prefix_length; 221 #endif 222 if (process_file) 223 { 224 entry.destination_filename = filename_to_add; 225 m_progress->FormatInformation("Found file in zip: '{}'", entry.destination_filename); 226 m_update_paths.push_back(std::move(entry)); 227 } 228 } 229 230 int res = unzGoToNextFile(m_zf); 231 if (res == UNZ_END_OF_LIST_OF_FILE) 232 break; 233 if (res != UNZ_OK) 234 { 235 m_progress->ModalError("unzGoToNextFile() failed"); 236 return false; 237 } 238 } 239 240 if (m_update_paths.empty()) 241 { 242 m_progress->ModalError("No files found in update zip."); 243 return false; 244 } 245 246 for (const FileToUpdate& ftu : m_update_paths) 247 { 248 const size_t len = ftu.destination_filename.length(); 249 for (size_t i = 0; i < len; i++) 250 { 251 if (ftu.destination_filename[i] == FS_OSPATH_SEPARATOR_CHARACTER) 252 { 253 std::string dir(ftu.destination_filename.begin(), ftu.destination_filename.begin() + i); 254 while (!dir.empty() && dir[dir.length() - 1] == FS_OSPATH_SEPARATOR_CHARACTER) 255 dir.erase(dir.length() - 1); 256 257 if (std::find(m_update_directories.begin(), m_update_directories.end(), dir) == m_update_directories.end()) 258 m_update_directories.push_back(std::move(dir)); 259 } 260 } 261 } 262 263 std::sort(m_update_directories.begin(), m_update_directories.end()); 264 for (const std::string& dir : m_update_directories) 265 m_progress->FormatDebugMessage("Directory: {}", dir); 266 267 return true; 268 } 269 270 bool Updater::PrepareStagingDirectory() 271 { 272 if (FileSystem::DirectoryExists(m_staging_directory.c_str())) 273 { 274 m_progress->DisplayWarning("Update staging directory already exists, removing"); 275 if (!RecursiveDeleteDirectory(m_staging_directory.c_str(), true) || 276 FileSystem::DirectoryExists(m_staging_directory.c_str())) 277 { 278 m_progress->ModalError("Failed to remove old staging directory"); 279 return false; 280 } 281 } 282 if (!FileSystem::CreateDirectory(m_staging_directory.c_str(), false)) 283 { 284 m_progress->FormatModalError("Failed to create staging directory {}", m_staging_directory); 285 return false; 286 } 287 288 // create subdirectories in staging directory 289 for (const std::string& subdir : m_update_directories) 290 { 291 m_progress->FormatInformation("Creating subdirectory in staging: {}", subdir); 292 293 const std::string staging_subdir = Path::Combine(m_staging_directory, subdir); 294 if (!FileSystem::CreateDirectory(staging_subdir.c_str(), false)) 295 { 296 m_progress->FormatModalError("Failed to create staging subdirectory {}", staging_subdir); 297 return false; 298 } 299 } 300 301 return true; 302 } 303 304 bool Updater::StageUpdate() 305 { 306 m_progress->SetProgressRange(static_cast<u32>(m_update_paths.size())); 307 m_progress->SetProgressValue(0); 308 309 for (const FileToUpdate& ftu : m_update_paths) 310 { 311 m_progress->FormatStatusText("Extracting '{}' (mode {:o})...", ftu.original_zip_filename, ftu.file_mode); 312 313 if (unzLocateFile(m_zf, ftu.original_zip_filename.c_str(), 0) != UNZ_OK) 314 { 315 m_progress->FormatModalError("Unable to locate file '{}' in zip", ftu.original_zip_filename); 316 return false; 317 } 318 else if (unzOpenCurrentFile(m_zf) != UNZ_OK) 319 { 320 m_progress->FormatModalError("Failed to open file '{}' in zip", ftu.original_zip_filename); 321 return false; 322 } 323 324 m_progress->FormatInformation("Extracting '{}'...", ftu.destination_filename); 325 326 const std::string destination_file = Path::Combine(m_staging_directory, ftu.destination_filename); 327 std::FILE* fp = FileSystem::OpenCFile(destination_file.c_str(), "wb"); 328 if (!fp) 329 { 330 m_progress->FormatModalError("Failed to open staging output file '{}'", destination_file); 331 unzCloseCurrentFile(m_zf); 332 return false; 333 } 334 335 static constexpr u32 CHUNK_SIZE = 4096; 336 u8 buffer[CHUNK_SIZE]; 337 for (;;) 338 { 339 int byte_count = unzReadCurrentFile(m_zf, buffer, CHUNK_SIZE); 340 if (byte_count < 0) 341 { 342 m_progress->FormatModalError("Failed to read file '{}' from zip", ftu.original_zip_filename); 343 std::fclose(fp); 344 FileSystem::DeleteFile(destination_file.c_str()); 345 unzCloseCurrentFile(m_zf); 346 return false; 347 } 348 else if (byte_count == 0) 349 { 350 // end of file 351 break; 352 } 353 354 if (std::fwrite(buffer, static_cast<size_t>(byte_count), 1, fp) != 1) 355 { 356 m_progress->FormatModalError("Failed to write to file '{}'", destination_file); 357 std::fclose(fp); 358 FileSystem::DeleteFile(destination_file.c_str()); 359 unzCloseCurrentFile(m_zf); 360 return false; 361 } 362 } 363 364 #ifndef _WIN32 365 if (ftu.file_mode != 0) 366 { 367 const int fd = fileno(fp); 368 const int res = (fd >= 0) ? fchmod(fd, ftu.file_mode) : -1; 369 if (res < 0) 370 { 371 m_progress->FormatModalError("Failed to set mode for file '{}' (fd {}) to {:o}: errno {}", destination_file, fd, 372 res, errno); 373 std::fclose(fp); 374 FileSystem::DeleteFile(destination_file.c_str()); 375 unzCloseCurrentFile(m_zf); 376 return false; 377 } 378 } 379 #endif 380 381 std::fclose(fp); 382 unzCloseCurrentFile(m_zf); 383 m_progress->IncrementProgressValue(); 384 } 385 386 return true; 387 } 388 389 bool Updater::CommitUpdate() 390 { 391 m_progress->SetStatusText("Committing update..."); 392 393 // create directories in target 394 for (const std::string& subdir : m_update_directories) 395 { 396 const std::string dest_subdir = Path::Combine(m_destination_directory, subdir); 397 if (!FileSystem::DirectoryExists(dest_subdir.c_str()) && !FileSystem::CreateDirectory(dest_subdir.c_str(), false)) 398 { 399 m_progress->FormatModalError("Failed to create target directory '{}'", dest_subdir); 400 return false; 401 } 402 } 403 404 // move files to target 405 for (const FileToUpdate& ftu : m_update_paths) 406 { 407 const std::string staging_file_name = Path::Combine(m_staging_directory, ftu.destination_filename); 408 const std::string dest_file_name = Path::Combine(m_destination_directory, ftu.destination_filename); 409 m_progress->FormatInformation("Moving '{}' to '{}'", staging_file_name, dest_file_name); 410 411 Error error; 412 #ifdef _WIN32 413 const bool result = MoveFileExW(FileSystem::GetWin32Path(staging_file_name).c_str(), 414 FileSystem::GetWin32Path(dest_file_name).c_str(), MOVEFILE_REPLACE_EXISTING); 415 if (!result) 416 error.SetWin32(GetLastError()); 417 #elif defined(__APPLE__) 418 const bool result = CocoaTools::MoveFile(staging_file_name.c_str(), dest_file_name.c_str(), &error); 419 #else 420 const bool result = (rename(staging_file_name.c_str(), dest_file_name.c_str()) == 0); 421 if (!result) 422 error.SetErrno(errno); 423 #endif 424 if (!result) 425 { 426 m_progress->FormatModalError("Failed to rename '{}' to '{}': {}", staging_file_name, dest_file_name, 427 error.GetDescription()); 428 return false; 429 } 430 } 431 432 return true; 433 } 434 435 void Updater::CleanupStagingDirectory() 436 { 437 // remove staging directory itself 438 if (!RecursiveDeleteDirectory(m_staging_directory.c_str(), true)) 439 m_progress->FormatError("Failed to remove staging directory '{}'", m_staging_directory); 440 } 441 442 bool Updater::ClearDestinationDirectory() 443 { 444 return RecursiveDeleteDirectory(m_destination_directory.c_str(), false); 445 }