Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/src/updater/updater.cpp
4243 views
1
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <[email protected]>
2
// SPDX-License-Identifier: 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
Error error;
137
FileSystem::FindResultsArray results;
138
if (FileSystem::FindFiles(path, "*", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_HIDDEN_FILES,
139
&results))
140
{
141
for (const FILESYSTEM_FIND_DATA& fd : results)
142
{
143
if ((fd.Attributes & (FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY | FILESYSTEM_FILE_ATTRIBUTE_LINK)) ==
144
FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY)
145
{
146
if (!RecursiveDeleteDirectory(fd.FileName.c_str(), true))
147
return false;
148
}
149
else
150
{
151
m_progress->FormatInformation("Removing file '{}'.", fd.FileName);
152
if (!FileSystem::DeleteFile(fd.FileName.c_str(), &error))
153
{
154
m_progress->FormatModalError("DeleteFile({}) failed: {}", fd.FileName, error.GetDescription());
155
return false;
156
}
157
}
158
}
159
}
160
161
if (!remove_dir)
162
return true;
163
164
m_progress->FormatInformation("Removing directory '{}'.", path);
165
if (!FileSystem::DeleteDirectory(path, &error))
166
{
167
m_progress->FormatModalError("DeleteDirectory({}) failed: {}", path, error.GetDescription());
168
return false;
169
}
170
171
return true;
172
#endif
173
}
174
175
bool Updater::ParseZip()
176
{
177
if (unzGoToFirstFile(m_zf) != UNZ_OK)
178
{
179
m_progress->ModalError("unzGoToFirstFile() failed");
180
return {};
181
}
182
183
for (;;)
184
{
185
char zip_filename_buffer[256];
186
unz_file_info64 file_info;
187
if (unzGetCurrentFileInfo64(m_zf, &file_info, zip_filename_buffer, sizeof(zip_filename_buffer), nullptr, 0, nullptr,
188
0) != UNZ_OK)
189
{
190
m_progress->ModalError("unzGetCurrentFileInfo64() failed");
191
return false;
192
}
193
194
FileToUpdate entry;
195
entry.original_zip_filename = zip_filename_buffer;
196
197
// replace forward slashes with backslashes
198
size_t len = std::strlen(zip_filename_buffer);
199
for (size_t i = 0; i < len; i++)
200
{
201
if (zip_filename_buffer[i] == '/' || zip_filename_buffer[i] == '\\')
202
zip_filename_buffer[i] = FS_OSPATH_SEPARATOR_CHARACTER;
203
}
204
205
// should never have a leading slash. just in case.
206
while (zip_filename_buffer[0] == FS_OSPATH_SEPARATOR_CHARACTER)
207
std::memmove(&zip_filename_buffer[1], &zip_filename_buffer[0], --len);
208
209
#ifdef _WIN32
210
entry.file_mode = 0;
211
#else
212
// Preserve permissions on Unix.
213
static constexpr u32 PERMISSION_MASK = (S_IRWXO | S_IRWXG | S_IRWXU);
214
entry.file_mode =
215
((file_info.external_fa >> 16) & 0x01FFu) & PERMISSION_MASK; // https://stackoverflow.com/a/28753385
216
#endif
217
218
// skip directories (we sort them out later)
219
if (len > 0 && zip_filename_buffer[len - 1] != FS_OSPATH_SEPARATOR_CHARACTER)
220
{
221
bool process_file = true;
222
const char* filename_to_add = zip_filename_buffer;
223
#ifdef _WIN32
224
// skip updater itself, since it was already pre-extracted.
225
process_file = process_file && (StringUtil::Strcasecmp(zip_filename_buffer, "updater.exe") != 0);
226
#elif defined(__APPLE__)
227
// on MacOS, we want to remove the DuckStation.app prefix.
228
static constexpr const char* PREFIX_PATH = "DuckStation.app/";
229
const size_t prefix_length = std::strlen(PREFIX_PATH);
230
process_file = process_file && (std::strncmp(zip_filename_buffer, PREFIX_PATH, prefix_length) == 0);
231
filename_to_add += prefix_length;
232
#endif
233
if (process_file)
234
{
235
entry.destination_filename = filename_to_add;
236
m_progress->FormatInformation("Found file in zip: '{}'", entry.destination_filename);
237
m_update_paths.push_back(std::move(entry));
238
}
239
}
240
241
int res = unzGoToNextFile(m_zf);
242
if (res == UNZ_END_OF_LIST_OF_FILE)
243
break;
244
if (res != UNZ_OK)
245
{
246
m_progress->ModalError("unzGoToNextFile() failed");
247
return false;
248
}
249
}
250
251
if (m_update_paths.empty())
252
{
253
m_progress->ModalError("No files found in update zip.");
254
return false;
255
}
256
257
for (const FileToUpdate& ftu : m_update_paths)
258
{
259
const size_t len = ftu.destination_filename.length();
260
for (size_t i = 0; i < len; i++)
261
{
262
if (ftu.destination_filename[i] == FS_OSPATH_SEPARATOR_CHARACTER)
263
{
264
std::string dir(ftu.destination_filename.begin(), ftu.destination_filename.begin() + i);
265
while (!dir.empty() && dir[dir.length() - 1] == FS_OSPATH_SEPARATOR_CHARACTER)
266
dir.erase(dir.length() - 1);
267
268
if (std::find(m_update_directories.begin(), m_update_directories.end(), dir) == m_update_directories.end())
269
m_update_directories.push_back(std::move(dir));
270
}
271
}
272
}
273
274
std::sort(m_update_directories.begin(), m_update_directories.end());
275
for (const std::string& dir : m_update_directories)
276
m_progress->FormatDebugMessage("Directory: {}", dir);
277
278
return true;
279
}
280
281
bool Updater::PrepareStagingDirectory()
282
{
283
if (FileSystem::DirectoryExists(m_staging_directory.c_str()))
284
{
285
m_progress->DisplayWarning("Update staging directory already exists, removing");
286
if (!RecursiveDeleteDirectory(m_staging_directory.c_str(), true) ||
287
FileSystem::DirectoryExists(m_staging_directory.c_str()))
288
{
289
m_progress->ModalError("Failed to remove old staging directory");
290
return false;
291
}
292
}
293
if (!FileSystem::CreateDirectory(m_staging_directory.c_str(), false))
294
{
295
m_progress->FormatModalError("Failed to create staging directory {}", m_staging_directory);
296
return false;
297
}
298
299
// create subdirectories in staging directory
300
for (const std::string& subdir : m_update_directories)
301
{
302
m_progress->FormatInformation("Creating subdirectory in staging: {}", subdir);
303
304
const std::string staging_subdir = Path::Combine(m_staging_directory, subdir);
305
if (!FileSystem::CreateDirectory(staging_subdir.c_str(), false))
306
{
307
m_progress->FormatModalError("Failed to create staging subdirectory {}", staging_subdir);
308
return false;
309
}
310
}
311
312
return true;
313
}
314
315
bool Updater::StageUpdate()
316
{
317
m_progress->SetProgressRange(static_cast<u32>(m_update_paths.size()));
318
m_progress->SetProgressValue(0);
319
320
for (const FileToUpdate& ftu : m_update_paths)
321
{
322
m_progress->FormatStatusText("Extracting '{}' (mode {:o})...", ftu.original_zip_filename, ftu.file_mode);
323
324
if (unzLocateFile(m_zf, ftu.original_zip_filename.c_str(), 0) != UNZ_OK)
325
{
326
m_progress->FormatModalError("Unable to locate file '{}' in zip", ftu.original_zip_filename);
327
return false;
328
}
329
else if (unzOpenCurrentFile(m_zf) != UNZ_OK)
330
{
331
m_progress->FormatModalError("Failed to open file '{}' in zip", ftu.original_zip_filename);
332
return false;
333
}
334
335
m_progress->FormatInformation("Extracting '{}'...", ftu.destination_filename);
336
337
const std::string destination_file = Path::Combine(m_staging_directory, ftu.destination_filename);
338
std::FILE* fp = FileSystem::OpenCFile(destination_file.c_str(), "wb");
339
if (!fp)
340
{
341
m_progress->FormatModalError("Failed to open staging output file '{}'", destination_file);
342
unzCloseCurrentFile(m_zf);
343
return false;
344
}
345
346
static constexpr u32 CHUNK_SIZE = 4096;
347
u8 buffer[CHUNK_SIZE];
348
for (;;)
349
{
350
int byte_count = unzReadCurrentFile(m_zf, buffer, CHUNK_SIZE);
351
if (byte_count < 0)
352
{
353
m_progress->FormatModalError("Failed to read file '{}' from zip", ftu.original_zip_filename);
354
std::fclose(fp);
355
FileSystem::DeleteFile(destination_file.c_str());
356
unzCloseCurrentFile(m_zf);
357
return false;
358
}
359
else if (byte_count == 0)
360
{
361
// end of file
362
break;
363
}
364
365
if (std::fwrite(buffer, static_cast<size_t>(byte_count), 1, fp) != 1)
366
{
367
m_progress->FormatModalError("Failed to write to file '{}'", destination_file);
368
std::fclose(fp);
369
FileSystem::DeleteFile(destination_file.c_str());
370
unzCloseCurrentFile(m_zf);
371
return false;
372
}
373
}
374
375
#ifndef _WIN32
376
if (ftu.file_mode != 0)
377
{
378
const int fd = fileno(fp);
379
const int res = (fd >= 0) ? fchmod(fd, ftu.file_mode) : -1;
380
if (res < 0)
381
{
382
m_progress->FormatModalError("Failed to set mode for file '{}' (fd {}) to {:o}: errno {}", destination_file, fd,
383
res, errno);
384
std::fclose(fp);
385
FileSystem::DeleteFile(destination_file.c_str());
386
unzCloseCurrentFile(m_zf);
387
return false;
388
}
389
}
390
#endif
391
392
std::fclose(fp);
393
unzCloseCurrentFile(m_zf);
394
m_progress->IncrementProgressValue();
395
}
396
397
return true;
398
}
399
400
bool Updater::CommitUpdate()
401
{
402
m_progress->SetStatusText("Committing update...");
403
404
// create directories in target
405
for (const std::string& subdir : m_update_directories)
406
{
407
const std::string dest_subdir = Path::Combine(m_destination_directory, subdir);
408
if (!FileSystem::DirectoryExists(dest_subdir.c_str()) && !FileSystem::CreateDirectory(dest_subdir.c_str(), false))
409
{
410
m_progress->FormatModalError("Failed to create target directory '{}'", dest_subdir);
411
return false;
412
}
413
}
414
415
// move files to target
416
for (const FileToUpdate& ftu : m_update_paths)
417
{
418
const std::string staging_file_name = Path::Combine(m_staging_directory, ftu.destination_filename);
419
const std::string dest_file_name = Path::Combine(m_destination_directory, ftu.destination_filename);
420
m_progress->FormatInformation("Moving '{}' to '{}'", staging_file_name, dest_file_name);
421
422
Error error;
423
#ifdef _WIN32
424
const bool result = MoveFileExW(FileSystem::GetWin32Path(staging_file_name).c_str(),
425
FileSystem::GetWin32Path(dest_file_name).c_str(), MOVEFILE_REPLACE_EXISTING);
426
if (!result)
427
error.SetWin32(GetLastError());
428
#elif defined(__APPLE__)
429
const bool result = CocoaTools::MoveFile(staging_file_name.c_str(), dest_file_name.c_str(), &error);
430
#else
431
const bool result = (rename(staging_file_name.c_str(), dest_file_name.c_str()) == 0);
432
if (!result)
433
error.SetErrno(errno);
434
#endif
435
if (!result)
436
{
437
m_progress->FormatModalError("Failed to rename '{}' to '{}': {}", staging_file_name, dest_file_name,
438
error.GetDescription());
439
return false;
440
}
441
}
442
443
return true;
444
}
445
446
void Updater::CleanupStagingDirectory()
447
{
448
// remove staging directory itself
449
if (!RecursiveDeleteDirectory(m_staging_directory.c_str(), true))
450
m_progress->FormatError("Failed to remove staging directory '{}'", m_staging_directory);
451
}
452
453
bool Updater::ClearDestinationDirectory()
454
{
455
return RecursiveDeleteDirectory(m_destination_directory.c_str(), false);
456
}
457
458