Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/src/uninstaller/main.cpp
7197 views
1
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <[email protected]>
2
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
3
4
#include "common/error.h"
5
#include "common/file_system.h"
6
#include "common/path.h"
7
#include "common/scoped_guard.h"
8
#include "common/string_util.h"
9
#include "common/windows_headers.h"
10
11
#include "installer/installer_params.h"
12
13
#include <fmt/format.h>
14
15
#include <KnownFolders.h>
16
#include <ShlObj.h>
17
#include <Shobjidl.h>
18
#include <combaseapi.h>
19
#include <shellapi.h>
20
#include <wrl/client.h>
21
22
static constexpr const wchar_t* WINDOW_TITLE = L"DuckStation Uninstaller";
23
24
template<typename... T>
25
static int FormatMessageBox(HWND hwnd, UINT type, fmt::format_string<T...> fmt, T&&... args)
26
{
27
const std::string message = fmt::format(fmt, std::forward<T>(args)...);
28
return MessageBoxW(hwnd, StringUtil::UTF8StringToWideString(message).c_str(), WINDOW_TITLE, type);
29
}
30
31
static std::string GetTempDirectory()
32
{
33
wchar_t temp_path[MAX_PATH];
34
if (GetTempPathW(MAX_PATH, temp_path) == 0)
35
return {};
36
37
return StringUtil::WideStringToUTF8String(temp_path);
38
}
39
40
static bool RecursiveDeleteDirectory(const std::string& path)
41
{
42
if (!FileSystem::DirectoryExists(path.c_str()))
43
return true;
44
45
Microsoft::WRL::ComPtr<IFileOperation> fo;
46
HRESULT hr = CoCreateInstance(CLSID_FileOperation, NULL, CLSCTX_ALL, IID_PPV_ARGS(fo.ReleaseAndGetAddressOf()));
47
if (FAILED(hr))
48
{
49
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "CoCreateInstance() for IFileOperation failed:\n{}",
50
Error::CreateHResult(hr).GetDescription());
51
return false;
52
}
53
54
Microsoft::WRL::ComPtr<IShellItem> item;
55
hr = SHCreateItemFromParsingName(StringUtil::UTF8StringToWideString(path).c_str(), NULL,
56
IID_PPV_ARGS(item.ReleaseAndGetAddressOf()));
57
if (FAILED(hr))
58
{
59
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "SHCreateItemFromParsingName() failed:\n{}",
60
Error::CreateHResult(hr).GetDescription());
61
return false;
62
}
63
64
hr = fo->SetOperationFlags(FOF_NOCONFIRMATION | FOF_SILENT);
65
if (FAILED(hr))
66
{
67
FormatMessageBox(nullptr, MB_ICONWARNING | MB_OK, "IFileOperation::SetOperationFlags() failed: {}",
68
Error::CreateHResult(hr).GetDescription());
69
}
70
71
hr = fo->DeleteItem(item.Get(), nullptr);
72
if (FAILED(hr))
73
{
74
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "IFileOperation::DeleteItem() failed:\n{}",
75
Error::CreateHResult(hr).GetDescription());
76
return false;
77
}
78
79
item.Reset();
80
hr = fo->PerformOperations();
81
if (FAILED(hr))
82
{
83
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "IFileOperation::PerformOperations() failed:\n{}",
84
Error::CreateHResult(hr).GetDescription());
85
return false;
86
}
87
88
return true;
89
}
90
91
static bool RemoveDesktopShortcut()
92
{
93
PWSTR desktop_folder = nullptr;
94
HRESULT hr = SHGetKnownFolderPath(FOLDERID_Desktop, 0, nullptr, &desktop_folder);
95
if (FAILED(hr))
96
{
97
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "SHGetKnownFolderPath(FOLDERID_Desktop) failed: {}",
98
Error::CreateHResult(hr).GetDescription());
99
return false;
100
}
101
102
const std::string shortcut_path =
103
Path::Combine(StringUtil::WideStringToUTF8String(desktop_folder), INSTALLER_SHORTCUT_FILENAME);
104
CoTaskMemFree(desktop_folder);
105
106
if (!FileSystem::FileExists(shortcut_path.c_str()))
107
return true;
108
109
Error error;
110
if (!FileSystem::DeleteFile(shortcut_path.c_str(), &error))
111
{
112
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "Failed to delete desktop shortcut: {}", error.GetDescription());
113
return false;
114
}
115
116
return true;
117
}
118
119
static bool RemoveStartMenuShortcut()
120
{
121
PWSTR programs_folder = nullptr;
122
HRESULT hr = SHGetKnownFolderPath(FOLDERID_Programs, 0, nullptr, &programs_folder);
123
if (FAILED(hr))
124
{
125
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "SHGetKnownFolderPath(FOLDERID_Programs) failed: {}",
126
Error::CreateHResult(hr).GetDescription());
127
return false;
128
}
129
130
const std::string shortcut_path =
131
Path::Combine(StringUtil::WideStringToUTF8String(programs_folder), INSTALLER_SHORTCUT_FILENAME);
132
CoTaskMemFree(programs_folder);
133
134
if (!FileSystem::FileExists(shortcut_path.c_str()))
135
return true;
136
137
Error error;
138
if (!FileSystem::DeleteFile(shortcut_path.c_str(), &error))
139
{
140
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "Failed to delete Start Menu shortcut: ", error.GetDescription());
141
return false;
142
}
143
144
return true;
145
}
146
147
static bool RemoveUninstallerEntry()
148
{
149
const LSTATUS status = RegDeleteTreeW(HKEY_CURRENT_USER, INSTALLER_UNINSTALL_REG_KEY);
150
if (status != ERROR_SUCCESS && status != ERROR_FILE_NOT_FOUND)
151
{
152
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "Failed to delete uninstaller registry key: {}",
153
Error::CreateWin32(status).GetDescription());
154
return false;
155
}
156
157
return true;
158
}
159
160
static std::string GetUserDataDirectory()
161
{
162
// Check LocalAppData\DuckStation (new location)
163
PWSTR appdata_directory = nullptr;
164
if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &appdata_directory)))
165
{
166
if (std::wcslen(appdata_directory) > 0)
167
{
168
std::string path = Path::Combine(StringUtil::WideStringToUTF8String(appdata_directory), "DuckStation");
169
CoTaskMemFree(appdata_directory);
170
if (FileSystem::DirectoryExists(path.c_str()))
171
return path;
172
}
173
else
174
{
175
CoTaskMemFree(appdata_directory);
176
}
177
}
178
179
return {};
180
}
181
182
static bool PerformUninstall(const std::string& install_directory)
183
{
184
RemoveDesktopShortcut();
185
RemoveStartMenuShortcut();
186
RemoveUninstallerEntry();
187
188
// Remove the installation directory
189
if (!RecursiveDeleteDirectory(install_directory))
190
return false;
191
192
// Check if user wants to remove user data
193
const std::string user_data_dir = GetUserDataDirectory();
194
if (!user_data_dir.empty() && FileSystem::DirectoryExists(user_data_dir.c_str()))
195
{
196
if (FormatMessageBox(nullptr, MB_ICONWARNING | MB_YESNO,
197
"Do you want to remove user data (save games, memory cards, settings)?\n\n"
198
"Location: {}\n\n"
199
"WARNING: This data cannot be recovered once deleted!",
200
user_data_dir) == IDYES)
201
{
202
RecursiveDeleteDirectory(user_data_dir);
203
}
204
}
205
206
MessageBoxW(nullptr, L"DuckStation has been uninstalled.", WINDOW_TITLE, MB_ICONINFORMATION | MB_OK);
207
return true;
208
}
209
210
static bool CopyToTempAndRelaunch(const std::string& current_exe_path, const std::string_view& install_directory)
211
{
212
const std::string temp_dir = GetTempDirectory();
213
if (temp_dir.empty())
214
{
215
MessageBoxW(nullptr, L"Failed to get temporary directory.", WINDOW_TITLE, MB_ICONERROR | MB_OK);
216
return false;
217
}
218
219
const std::string temp_exe_path = Path::Combine(temp_dir, "duckstation_uninstall.exe");
220
221
Error error;
222
if (!FileSystem::CopyFilePath(current_exe_path.c_str(), temp_exe_path.c_str(), true, &error))
223
{
224
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "Failed to copy uninstaller to temporary location:\n{}",
225
error.GetDescription());
226
return false;
227
}
228
229
// Quote the install directory in case it contains spaces
230
const std::wstring args_w = StringUtil::UTF8StringToWideString(fmt::format("\"{}\"", install_directory));
231
const std::wstring temp_exe_path_w = StringUtil::UTF8StringToWideString(temp_exe_path);
232
233
SHELLEXECUTEINFOW sei = {};
234
sei.cbSize = sizeof(sei);
235
sei.fMask = SEE_MASK_DEFAULT;
236
sei.lpFile = temp_exe_path_w.c_str();
237
sei.lpParameters = args_w.c_str();
238
sei.nShow = SW_SHOWNORMAL;
239
240
if (!ShellExecuteExW(&sei))
241
{
242
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "Failed to launch uninstaller from temporary location:\n{}",
243
Error::CreateWin32(GetLastError()).GetDescription());
244
FileSystem::DeleteFile(temp_exe_path.c_str());
245
return false;
246
}
247
248
return true;
249
}
250
251
static int RunFromInstallDirectory()
252
{
253
Error error;
254
const std::string current_exe_path = FileSystem::GetProgramPath(&error);
255
if (current_exe_path.empty())
256
{
257
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "Failed to get current executable path:\n{}",
258
error.GetDescription());
259
return EXIT_FAILURE;
260
}
261
262
const std::string_view install_directory = Path::GetDirectory(current_exe_path);
263
264
// Verify this is a DuckStation installation by checking for the main executable
265
const std::string main_exe_path = Path::Combine(install_directory, INSTALLER_PROGRAM_FILENAME);
266
if (!FileSystem::FileExists(main_exe_path.c_str()))
267
{
268
MessageBoxW(nullptr,
269
L"This does not appear to be a valid DuckStation installation.\n\n"
270
L"The main executable was not found in the same directory as the uninstaller.",
271
WINDOW_TITLE, MB_ICONERROR | MB_OK);
272
return EXIT_FAILURE;
273
}
274
275
// Confirm uninstallation with user
276
if (FormatMessageBox(nullptr, MB_ICONQUESTION | MB_YESNO,
277
"This will uninstall DuckStation from:\n\n{}\n\n"
278
"Do you want to continue?",
279
install_directory) != IDYES)
280
{
281
return EXIT_SUCCESS;
282
}
283
284
// Copy to temp and relaunch
285
if (!CopyToTempAndRelaunch(current_exe_path, install_directory))
286
return EXIT_FAILURE;
287
288
return EXIT_SUCCESS;
289
}
290
291
static int RunFromTemp(const std::string& install_directory)
292
{
293
const bool success = PerformUninstall(install_directory);
294
295
#if 0
296
// Schedule ourselves for deletion on reboot
297
const std::string current_exe_path = FileSystem::GetProgramPath(nullptr);
298
if (!current_exe_path.empty())
299
{
300
const std::wstring current_exe_path_w = FileSystem::GetWin32Path(current_exe_path);
301
if (!current_exe_path_w.empty())
302
{
303
// TODO: This requires admin rights. Find a better way.
304
if (!MoveFileExW(current_exe_path_w.c_str(), nullptr, MOVEFILE_DELAY_UNTIL_REBOOT))
305
{
306
FormatMessageBox(nullptr, MB_ICONERROR | MB_OK, "Failed to schedule temp uninstaller for deletion: {}",
307
Error::CreateWin32(GetLastError()).GetDescription());
308
}
309
}
310
}
311
#endif
312
313
return success ? EXIT_SUCCESS : EXIT_FAILURE;
314
}
315
316
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nShowCmd)
317
{
318
// IFileOperation requires single-threaded apartment mode
319
const bool com_initialized = SUCCEEDED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED));
320
const ScopedGuard com_guard = [com_initialized]() {
321
if (com_initialized)
322
CoUninitialize();
323
};
324
325
// Parse command line - we only accept zero or one argument
326
int argc = 0;
327
LPWSTR* argv = nullptr;
328
if (lpCmdLine && std::wcslen(lpCmdLine) > 0 && !(argv = CommandLineToArgvW(lpCmdLine, &argc)))
329
{
330
MessageBoxW(nullptr, L"Failed to parse command line.", WINDOW_TITLE, MB_ICONERROR | MB_OK);
331
return EXIT_FAILURE;
332
}
333
334
const ScopedGuard argv_guard = [argv]() {
335
if (argv)
336
LocalFree(argv);
337
};
338
339
// If no arguments, we're running from the install directory - copy to temp and relaunch
340
if (argc < 1 || (argc == 1 && std::wcslen(argv[0]) == 0))
341
return RunFromInstallDirectory();
342
343
// App uninstaller sets argv[0] to the program name.
344
if (argc == 1)
345
{
346
const std::string install_directory = StringUtil::WideStringToUTF8String(argv[0]);
347
if (FileSystem::DirectoryExists(install_directory.c_str()))
348
return RunFromTemp(install_directory);
349
else
350
return RunFromInstallDirectory();
351
}
352
353
// Too many arguments
354
MessageBoxW(nullptr, L"Invalid command line arguments.\n\nUsage: uninstaller.exe [install_directory]", WINDOW_TITLE,
355
MB_ICONERROR | MB_OK);
356
return EXIT_FAILURE;
357
}
358
359