/ src / modules / Workspaces / WorkspacesLib / AppUtils.cpp
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  }