AppUtils.cpp
1 #include "pch.h" 2 #include "AppUtils.h" 3 #include "SteamHelper.h" 4 5 #include <atlbase.h> 6 #include <propvarutil.h> 7 #include <ShlObj.h> 8 #include <TlHelp32.h> 9 10 #include <filesystem> 11 12 #include <common/logger/logger.h> 13 #include <common/utils/process_path.h> 14 #include <common/utils/winapi_error.h> 15 16 namespace Utils 17 { 18 namespace Apps 19 { 20 namespace NonLocalizable 21 { 22 constexpr const wchar_t* PackageFullNameProp = L"System.AppUserModel.PackageFullName"; 23 constexpr const wchar_t* PackageInstallPathProp = L"System.AppUserModel.PackageInstallPath"; 24 constexpr const wchar_t* InstallPathProp = L"System.Link.TargetParsingPath"; 25 constexpr const wchar_t* HostEnvironmentProp = L"System.AppUserModel.HostEnvironment"; 26 constexpr const wchar_t* AppUserModelIdProp = L"System.AppUserModel.ID"; 27 28 constexpr const wchar_t* FileExplorerName = L"File Explorer"; 29 constexpr const wchar_t* FileExplorerPath = L"C:\\WINDOWS\\EXPLORER.EXE"; 30 constexpr const wchar_t* PowerToys = L"PowerToys.exe"; 31 constexpr const wchar_t* PowerToysSettingsUpper = L"POWERTOYS.SETTINGS.EXE"; 32 constexpr const wchar_t* PowerToysSettings = L"PowerToys.Settings.exe"; 33 constexpr const wchar_t* ApplicationFrameHost = L"APPLICATIONFRAMEHOST.EXE"; 34 constexpr const wchar_t* Exe = L".EXE"; 35 36 constexpr const wchar_t* EdgeFilename = L"msedge.exe"; 37 constexpr const wchar_t* ChromeFilename = L"chrome.exe"; 38 39 constexpr const wchar_t* SteamUrlProtocol = L"steam:"; 40 } 41 42 AppList IterateAppsFolder() 43 { 44 AppList result{}; 45 46 // get apps folder 47 CComPtr<IShellItem> folder; 48 HRESULT hr = SHGetKnownFolderItem(FOLDERID_AppsFolder, KF_FLAG_DEFAULT, nullptr, IID_PPV_ARGS(&folder)); 49 if (FAILED(hr)) 50 { 51 Logger::error(L"Failed to get known apps folder: {}", get_last_error_or_default(hr)); 52 return result; 53 } 54 55 CComPtr<IEnumShellItems> enumItems; 56 hr = folder->BindToHandler(nullptr, BHID_EnumItems, IID_PPV_ARGS(&enumItems)); 57 if (FAILED(hr)) 58 { 59 Logger::error(L"Failed to bind to enum items handler: {}", get_last_error_or_default(hr)); 60 return result; 61 } 62 63 IShellItem* items; 64 while (enumItems->Next(1, &items, nullptr) == S_OK) 65 { 66 CComPtr<IShellItem> item = items; 67 CComHeapPtr<wchar_t> name; 68 69 hr = item->GetDisplayName(SIGDN_NORMALDISPLAY, &name); 70 if (FAILED(hr)) 71 { 72 Logger::error(L"Failed to get display name for app: {}", get_last_error_or_default(hr)); 73 continue; 74 } 75 76 AppData data{ 77 .name = std::wstring(name.m_pData), 78 }; 79 80 // properties 81 CComPtr<IPropertyStore> store; 82 hr = item->BindToHandler(NULL, BHID_PropertyStore, IID_PPV_ARGS(&store)); 83 if (FAILED(hr)) 84 { 85 Logger::error(L"Failed to bind to property store handler: {}", get_last_error_or_default(hr)); 86 continue; 87 } 88 89 DWORD count = 0; 90 store->GetCount(&count); 91 for (DWORD i = 0; i < count; i++) 92 { 93 PROPERTYKEY pk; 94 hr = store->GetAt(i, &pk); 95 if (FAILED(hr)) 96 { 97 Logger::error(L"Failed to get property key: {}", get_last_error_or_default(hr)); 98 continue; 99 } 100 101 CComHeapPtr<wchar_t> pkName; 102 PSGetNameFromPropertyKey(pk, &pkName); 103 104 std::wstring prop(pkName.m_pData); 105 if (prop == NonLocalizable::PackageFullNameProp || 106 prop == NonLocalizable::PackageInstallPathProp || 107 prop == NonLocalizable::InstallPathProp || 108 prop == NonLocalizable::HostEnvironmentProp || 109 prop == NonLocalizable::AppUserModelIdProp) 110 { 111 PROPVARIANT pv; 112 PropVariantInit(&pv); 113 hr = store->GetValue(pk, &pv); 114 if (SUCCEEDED(hr)) 115 { 116 if (prop == NonLocalizable::HostEnvironmentProp) 117 { 118 CComHeapPtr<int> propVariantInt; 119 propVariantInt.Allocate(1); 120 PropVariantToInt32(pv, propVariantInt); 121 122 if (prop == NonLocalizable::HostEnvironmentProp) 123 { 124 data.canLaunchElevated = *propVariantInt.m_pData != 1; 125 } 126 } 127 else 128 { 129 CComHeapPtr<wchar_t> propVariantString; 130 propVariantString.Allocate(512); 131 PropVariantToString(pv, propVariantString, 512); 132 133 if (prop == NonLocalizable::PackageFullNameProp) 134 { 135 data.packageFullName = propVariantString.m_pData; 136 } 137 else if (prop == NonLocalizable::AppUserModelIdProp) 138 { 139 data.appUserModelId = propVariantString.m_pData; 140 } 141 else if (prop == NonLocalizable::PackageInstallPathProp || prop == NonLocalizable::InstallPathProp) 142 { 143 data.installPath = propVariantString.m_pData; 144 145 if (!data.installPath.empty()) 146 { 147 const bool isSteamProtocol = data.installPath.rfind(NonLocalizable::SteamUrlProtocol, 0) == 0; 148 149 if (isSteamProtocol) 150 { 151 Logger::info(L"Found steam game: protocol path: {}", data.installPath); 152 data.protocolPath = data.installPath; 153 154 try 155 { 156 auto gameId = Steam::GetGameIdFromUrlProtocolPath(data.installPath); 157 auto gameFolder = Steam::GetSteamGameInfoFromAcfFile(gameId); 158 159 if (gameFolder) 160 { 161 data.installPath = gameFolder->gameInstallationPath; 162 Logger::info(L"Found steam game: physical path: {}", data.installPath); 163 } 164 } 165 catch (std::exception ex) 166 { 167 Logger::error(L"Failed to get installPath for game {}", data.installPath); 168 Logger::error("Error: {}", ex.what()); 169 } 170 } 171 } 172 } 173 } 174 175 PropVariantClear(&pv); 176 } 177 else 178 { 179 Logger::error(L"Failed to get property value: {}", get_last_error_or_default(hr)); 180 } 181 } 182 } 183 184 if (!data.name.empty()) 185 { 186 result.push_back(data); 187 } 188 } 189 190 return result; 191 } 192 193 const std::wstring& GetCurrentFolder() 194 { 195 static std::wstring currentFolder; 196 if (currentFolder.empty()) 197 { 198 TCHAR buffer[MAX_PATH] = { 0 }; 199 GetModuleFileName(NULL, buffer, MAX_PATH); 200 std::wstring::size_type pos = std::wstring(buffer).find_last_of(L"\\/"); 201 currentFolder = std::wstring(buffer).substr(0, pos); 202 } 203 204 return currentFolder; 205 } 206 207 const std::wstring& GetCurrentFolderUpper() 208 { 209 static std::wstring currentFolderUpper; 210 if (currentFolderUpper.empty()) 211 { 212 currentFolderUpper = GetCurrentFolder(); 213 std::transform(currentFolderUpper.begin(), currentFolderUpper.end(), currentFolderUpper.begin(), towupper); 214 } 215 216 return currentFolderUpper; 217 } 218 219 AppList GetAppsList() 220 { 221 return IterateAppsFolder(); 222 } 223 224 DWORD GetParentPid(DWORD pid) 225 { 226 DWORD res = 0; 227 HANDLE h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); 228 PROCESSENTRY32 pe = { 0 }; 229 pe.dwSize = sizeof(PROCESSENTRY32); 230 231 if (Process32First(h, &pe)) 232 { 233 do 234 { 235 if (pe.th32ProcessID == pid) 236 { 237 res = pe.th32ParentProcessID; 238 } 239 } while (Process32Next(h, &pe)); 240 } 241 242 CloseHandle(h); 243 return res; 244 } 245 246 std::optional<AppData> GetApp(const std::wstring& appPath, DWORD pid, const AppList& apps) 247 { 248 std::wstring appPathUpper(appPath); 249 std::transform(appPathUpper.begin(), appPathUpper.end(), appPathUpper.begin(), towupper); 250 251 // filter out ApplicationFrameHost.exe 252 if (appPathUpper.ends_with(NonLocalizable::ApplicationFrameHost)) 253 { 254 return std::nullopt; 255 } 256 257 // edge case, "Windows Software Development Kit" has the same app path as "File Explorer" 258 if (appPathUpper == NonLocalizable::FileExplorerPath) 259 { 260 return AppData{ 261 .name = NonLocalizable::FileExplorerName, 262 .installPath = appPath, 263 }; 264 } 265 266 // PowerToys 267 if (appPathUpper.contains(GetCurrentFolderUpper())) 268 { 269 if (appPathUpper.contains(NonLocalizable::PowerToysSettingsUpper)) 270 { 271 return AppData{ 272 .name = NonLocalizable::PowerToysSettings, 273 .installPath = GetCurrentFolder() + L"\\" + NonLocalizable::PowerToys 274 }; 275 } 276 else 277 { 278 return AppData{ 279 .name = std::filesystem::path(appPath).stem(), 280 .installPath = appPath, 281 }; 282 } 283 } 284 285 // search in apps list 286 std::optional<AppData> appDataPlanB{ std::nullopt }; 287 for (const auto& appData : apps) 288 { 289 if (!appData.installPath.empty()) 290 { 291 std::wstring installPathUpper(appData.installPath); 292 std::transform(installPathUpper.begin(), installPathUpper.end(), installPathUpper.begin(), towupper); 293 294 if (appPathUpper.contains(installPathUpper)) 295 { 296 // Update the install path to keep .exe in the path 297 if (!installPathUpper.ends_with(NonLocalizable::Exe)) 298 { 299 auto settingsAppData = appData; 300 settingsAppData.installPath = appPath; 301 return settingsAppData; 302 } 303 304 return appData; 305 } 306 307 // edge case, some apps (e.g., Gitkraken) have different .exe files in the subfolders. 308 // apps list contains only one path, so in this case app is not found 309 // remember the match and return it in case the loop is over and there are no direct matches 310 if (std::filesystem::path(appPath).filename() == std::filesystem::path(appData.installPath).filename()) 311 { 312 appDataPlanB = appData; 313 } 314 } 315 } 316 317 if (appDataPlanB.has_value()) 318 { 319 return appDataPlanB.value(); 320 } 321 322 // try by name if path not found 323 // apps list could contain a different path from that one we get from the process (for electron) 324 std::wstring exeName = std::filesystem::path(appPath).stem(); 325 std::wstring exeNameUpper(exeName); 326 std::transform(exeNameUpper.begin(), exeNameUpper.end(), exeNameUpper.begin(), towupper); 327 328 for (const auto& appData : apps) 329 { 330 std::wstring appNameUpper(appData.name); 331 std::transform(appNameUpper.begin(), appNameUpper.end(), appNameUpper.begin(), towupper); 332 333 if (appNameUpper == exeNameUpper) 334 { 335 auto result = appData; 336 result.installPath = appPath; 337 return result; 338 } 339 } 340 341 // try with parent process (fix for Steam) 342 auto parentPid = GetParentPid(pid); 343 auto parentProcessPath = get_process_path(parentPid); 344 345 if (!parentProcessPath.empty()) 346 { 347 std::wstring parentDirUpper = std::filesystem::path(parentProcessPath).parent_path().c_str(); 348 std::transform(parentDirUpper.begin(), parentDirUpper.end(), parentDirUpper.begin(), towupper); 349 350 if (appPathUpper.starts_with(parentDirUpper)) 351 { 352 Logger::info(L"original process is in the subfolder of the parent process"); 353 354 for (const auto& appData : apps) 355 { 356 if (!appData.installPath.empty()) 357 { 358 std::wstring installDirUpper = std::filesystem::path(appData.installPath).parent_path().c_str(); 359 std::transform(installDirUpper.begin(), installDirUpper.end(), installDirUpper.begin(), towupper); 360 361 if (installDirUpper == parentDirUpper) 362 { 363 return appData; 364 } 365 } 366 } 367 } 368 } 369 370 return AppData{ 371 .name = std::filesystem::path(appPath).stem(), 372 .installPath = appPath 373 }; 374 } 375 376 std::optional<AppData> GetApp(HWND window, const AppList& apps) 377 { 378 std::wstring processPath = get_process_path(window); 379 380 DWORD pid{}; 381 GetWindowThreadProcessId(window, &pid); 382 383 return Utils::Apps::GetApp(processPath, pid, apps); 384 } 385 386 bool UpdateAppVersion(WorkspacesData::WorkspacesProject::Application& app, const AppList& installedApps) 387 { 388 auto installedApp = std::find_if(installedApps.begin(), installedApps.end(), [&](const AppData& val) { return val.name == app.name; }); 389 if (installedApp == installedApps.end()) 390 { 391 return false; 392 } 393 394 // Packaged apps have version in the path, it will be outdated after update. 395 // We need make sure the current package is up to date. 396 if (!app.packageFullName.empty()) 397 { 398 if (app.packageFullName != installedApp->packageFullName) 399 { 400 std::wstring exeFileName = app.path.substr(app.path.find_last_of(L"\\") + 1); 401 app.packageFullName = installedApp->packageFullName; 402 app.path = installedApp->installPath + L"\\" + exeFileName; 403 Logger::trace(L"Updated package full name for {}: {}", app.name, app.packageFullName); 404 return true; 405 } 406 } 407 408 return false; 409 } 410 411 bool UpdateWorkspacesApps(WorkspacesData::WorkspacesProject& workspace, const AppList& installedApps) 412 { 413 bool updated = false; 414 for (auto& app : workspace.apps) 415 { 416 updated |= UpdateAppVersion(app, installedApps); 417 } 418 419 return updated; 420 } 421 422 bool AppData::IsEdge() const 423 { 424 return installPath.ends_with(NonLocalizable::EdgeFilename); 425 } 426 427 bool AppData::IsChrome() const 428 { 429 return installPath.ends_with(NonLocalizable::ChromeFilename); 430 } 431 432 bool AppData::IsSteamGame() const 433 { 434 return protocolPath.rfind(NonLocalizable::SteamUrlProtocol, 0) == 0; 435 } 436 } 437 }