A game about forced loneliness, made by TACStudios
1/*--------------------------------------------------------------------------------------------- 2 * Copyright (c) Unity Technologies. 3 * Copyright (c) Microsoft Corporation. All rights reserved. 4 * Licensed under the MIT License. See License.txt in the project root for license information. 5 *--------------------------------------------------------------------------------------------*/ 6 7#include <iostream> 8#include <sstream> 9#include <string> 10#include <filesystem> 11#include <windows.h> 12#include <shlwapi.h> 13 14#include <fcntl.h> 15#include <io.h> 16 17#include "BStrHolder.h" 18#include "ComPtr.h" 19#include "dte80a.tlh" 20 21constexpr int RETRY_INTERVAL_MS = 150; 22constexpr int TIMEOUT_MS = 10000; 23 24// Often a DTE call made to Visual Studio can fail after Visual Studio has just started. Usually the 25// return value will be RPC_E_CALL_REJECTED, meaning that Visual Studio is probably busy on another 26// thread. This types filter the RPC messages and retries to send the message until VS accepts it. 27class CRetryMessageFilter : public IMessageFilter 28{ 29private: 30 static bool ShouldRetryCall(DWORD dwTickCount, DWORD dwRejectType) 31 { 32 if (dwRejectType == SERVERCALL_RETRYLATER || dwRejectType == SERVERCALL_REJECTED) { 33 return dwTickCount < TIMEOUT_MS; 34 } 35 36 return false; 37 } 38 39 win::ComPtr<IMessageFilter> currentFilter; 40 41public: 42 CRetryMessageFilter() 43 { 44 HRESULT hr = CoRegisterMessageFilter(this, &currentFilter); 45 _ASSERT(SUCCEEDED(hr)); 46 } 47 48 ~CRetryMessageFilter() 49 { 50 win::ComPtr<IMessageFilter> messageFilter; 51 HRESULT hr = CoRegisterMessageFilter(currentFilter, &messageFilter); 52 _ASSERT(SUCCEEDED(hr)); 53 } 54 55 // IUnknown methods 56 IFACEMETHODIMP QueryInterface(REFIID riid, void** ppv) 57 { 58 static const QITAB qit[] = 59 { 60 QITABENT(CRetryMessageFilter, IMessageFilter), 61 { 0 }, 62 }; 63 return QISearch(this, qit, riid, ppv); 64 } 65 66 IFACEMETHODIMP_(ULONG) AddRef() 67 { 68 return 0; 69 } 70 71 IFACEMETHODIMP_(ULONG) Release() 72 { 73 return 0; 74 } 75 76 DWORD STDMETHODCALLTYPE HandleInComingCall(DWORD dwCallType, HTASK htaskCaller, DWORD dwTickCount, LPINTERFACEINFO lpInterfaceInfo) 77 { 78 if (currentFilter) 79 return currentFilter->HandleInComingCall(dwCallType, htaskCaller, dwTickCount, lpInterfaceInfo); 80 81 return SERVERCALL_ISHANDLED; 82 } 83 84 DWORD STDMETHODCALLTYPE RetryRejectedCall(HTASK htaskCallee, DWORD dwTickCount, DWORD dwRejectType) 85 { 86 if (ShouldRetryCall(dwTickCount, dwRejectType)) 87 return RETRY_INTERVAL_MS; 88 89 if (currentFilter) 90 return currentFilter->RetryRejectedCall(htaskCallee, dwTickCount, dwRejectType); 91 92 return (DWORD)-1; 93 } 94 95 DWORD STDMETHODCALLTYPE MessagePending(HTASK htaskCallee, DWORD dwTickCount, DWORD dwPendingType) 96 { 97 if (currentFilter) 98 return currentFilter->MessagePending(htaskCallee, dwTickCount, dwPendingType); 99 100 return PENDINGMSG_WAITDEFPROCESS; 101 } 102}; 103 104static void DisplayProgressbar() { 105 std::wcout << "displayProgressBar" << std::endl; 106} 107 108static void ClearProgressbar() { 109 std::wcout << "clearprogressbar" << std::endl; 110} 111 112inline const std::wstring QuoteString(const std::wstring& str) 113{ 114 return L"\"" + str + L"\""; 115} 116 117static std::wstring ErrorCodeToMsg(DWORD code) 118{ 119 LPWSTR msgBuf = nullptr; 120 if (!FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, 121 nullptr, code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)&msgBuf, 0, nullptr)) 122 { 123 return L"Unknown error"; 124 } 125 else 126 { 127 return msgBuf; 128 } 129} 130 131// Get an environment variable 132static std::wstring GetEnvironmentVariableValue(const std::wstring& variableName) { 133 DWORD currentBufferSize = MAX_PATH; 134 std::wstring variableValue; 135 variableValue.resize(currentBufferSize); 136 137 DWORD requiredBufferSize = GetEnvironmentVariableW(variableName.c_str(), variableValue.data(), currentBufferSize); 138 if (requiredBufferSize == 0) { 139 // Environment variable probably does not exist. 140 return std::wstring(); 141 } 142 143 if (currentBufferSize < requiredBufferSize) { 144 variableValue.resize(requiredBufferSize); 145 if (GetEnvironmentVariableW(variableName.c_str(), variableValue.data(), currentBufferSize) == 0) 146 return std::wstring(); 147 } 148 149 variableValue.resize(requiredBufferSize); 150 return variableValue; 151} 152 153static bool StartVisualStudioProcess( 154 const std::filesystem::path &visualStudioExecutablePath, 155 const std::filesystem::path &solutionPath, 156 DWORD *dwProcessId) { 157 158 STARTUPINFOW si; 159 PROCESS_INFORMATION pi; 160 BOOL result; 161 162 ZeroMemory(&si, sizeof(si)); 163 si.cb = sizeof(si); 164 ZeroMemory(&pi, sizeof(pi)); 165 166 std::wstring startingDirectory = visualStudioExecutablePath.parent_path(); 167 168 // Build the command line that is passed as the argv of the VS process 169 // argv[0] must be the quoted full path to the VS exe 170 std::wstringstream commandLineStream; 171 commandLineStream << QuoteString(visualStudioExecutablePath) << L" "; 172 173 std::wstring vsArgsWide = GetEnvironmentVariableValue(L"UNITY_VS_ARGS"); 174 if (!vsArgsWide.empty()) 175 commandLineStream << vsArgsWide << L" "; 176 177 commandLineStream << QuoteString(solutionPath); 178 179 std::wstring commandLine = commandLineStream.str(); 180 181 std::wcout << "Starting Visual Studio process with: " << commandLine << std::endl; 182 183 result = CreateProcessW( 184 visualStudioExecutablePath.c_str(), // Full path to VS, must not be quoted 185 commandLine.data(), // Command line, as passed as argv, separate arguments must be quoted if they contain spaces 186 nullptr, // Process handle not inheritable 187 nullptr, // Thread handle not inheritable 188 false, // Set handle inheritance to FALSE 189 0, // No creation flags 190 nullptr, // Use parent's environment block 191 startingDirectory.c_str(), // starting directory set to the VS directory 192 &si, 193 &pi); 194 195 if (!result) { 196 DWORD error = GetLastError(); 197 std::wcout << "Starting Visual Studio process failed: " << ErrorCodeToMsg(error) << std::endl; 198 return false; 199 } 200 201 *dwProcessId = pi.dwProcessId; 202 CloseHandle(pi.hProcess); 203 CloseHandle(pi.hThread); 204 205 return true; 206} 207 208static bool 209MonikerIsVisualStudioProcess(const win::ComPtr<IMoniker> &moniker, const win::ComPtr<IBindCtx> &bindCtx, const DWORD dwProcessId = 0) { 210 LPOLESTR oleMonikerName; 211 if (FAILED(moniker->GetDisplayName(bindCtx, nullptr, &oleMonikerName))) 212 return false; 213 214 std::wstring monikerName(oleMonikerName); 215 216 // VisualStudio Moniker is "!VisualStudio.DTE.$Version:$PID" 217 // Example "!VisualStudio.DTE.14.0:1234" 218 219 if (monikerName.find(L"!VisualStudio.DTE") != 0) 220 return false; 221 222 if (dwProcessId == 0) 223 return true; 224 225 std::wstringstream suffixStream; 226 suffixStream << ":"; 227 suffixStream << dwProcessId; 228 229 std::wstring suffix(suffixStream.str()); 230 231 return monikerName.length() - suffix.length() == monikerName.find(suffix); 232} 233 234static win::ComPtr<EnvDTE::_DTE> FindRunningVisualStudioWithSolution( 235 const std::filesystem::path &visualStudioExecutablePath, 236 const std::filesystem::path &solutionPath) 237{ 238 win::ComPtr<IUnknown> punk = nullptr; 239 win::ComPtr<EnvDTE::_DTE> dte = nullptr; 240 241 CRetryMessageFilter retryMessageFilter; 242 243 // Search through the Running Object Table for an instance of Visual Studio 244 // to use that either has the correct solution already open or does not have 245 // any solution open. 246 win::ComPtr<IRunningObjectTable> ROT; 247 if (FAILED(GetRunningObjectTable(0, &ROT))) 248 return nullptr; 249 250 win::ComPtr<IBindCtx> bindCtx; 251 if (FAILED(CreateBindCtx(0, &bindCtx))) 252 return nullptr; 253 254 win::ComPtr<IEnumMoniker> enumMoniker; 255 if (FAILED(ROT->EnumRunning(&enumMoniker))) 256 return nullptr; 257 258 win::ComPtr<IMoniker> moniker; 259 ULONG monikersFetched = 0; 260 while (SUCCEEDED(enumMoniker->Next(1, &moniker, &monikersFetched)) && monikersFetched) { 261 if (!MonikerIsVisualStudioProcess(moniker, bindCtx)) 262 continue; 263 264 if (FAILED(ROT->GetObject(moniker, &punk))) 265 continue; 266 267 punk.As(&dte); 268 if (!dte) 269 continue; 270 271 // Okay, so we found an actual running instance of Visual Studio. 272 273 // Get the executable path of this running instance. 274 BStrHolder visualStudioFullName; 275 if (FAILED(dte->get_FullName(&visualStudioFullName))) 276 continue; 277 278 std::filesystem::path currentVisualStudioExecutablePath = std::wstring(visualStudioFullName); 279 280 // Ask for its current solution. 281 win::ComPtr<EnvDTE::_Solution> solution; 282 if (FAILED(dte->get_Solution(&solution))) 283 continue; 284 285 // Get the name of that solution. 286 BStrHolder solutionFullName; 287 if (FAILED(solution->get_FullName(&solutionFullName))) 288 continue; 289 290 std::filesystem::path currentSolutionPath = std::wstring(solutionFullName); 291 if (currentSolutionPath.empty()) 292 continue; 293 294 std::wcout << "Visual Studio opened on " << currentSolutionPath.wstring() << std::endl; 295 296 // If the name matches the solution we want to open and we have a Visual Studio installation path to use and this one matches that path, then use it. 297 // If we don't have a Visual Studio installation path to use, just use this solution. 298 if (std::filesystem::equivalent(currentSolutionPath, solutionPath)) { 299 std::wcout << "We found a running Visual Studio session with the solution open." << std::endl; 300 if (!visualStudioExecutablePath.empty()) { 301 if (std::filesystem::equivalent(currentVisualStudioExecutablePath, visualStudioExecutablePath)) { 302 return dte; 303 } 304 else { 305 std::wcout << "This running Visual Studio session does not seem to be the version requested in the user preferences. We will keep looking." << std::endl; 306 } 307 } 308 else { 309 std::wcout << "We're not sure which version of Visual Studio was requested in the user preferences. We will use this running session." << std::endl; 310 return dte; 311 } 312 } 313 } 314 return nullptr; 315} 316 317static win::ComPtr<EnvDTE::_DTE> FindRunningVisualStudioWithPID(const DWORD dwProcessId) { 318 win::ComPtr<IUnknown> punk = nullptr; 319 win::ComPtr<EnvDTE::_DTE> dte = nullptr; 320 321 // Search through the Running Object Table for a Visual Studio 322 // process with the process ID specified 323 win::ComPtr<IRunningObjectTable> ROT; 324 if (FAILED(GetRunningObjectTable(0, &ROT))) 325 return nullptr; 326 327 win::ComPtr<IBindCtx> bindCtx; 328 if (FAILED(CreateBindCtx(0, &bindCtx))) 329 return nullptr; 330 331 win::ComPtr<IEnumMoniker> enumMoniker; 332 if (FAILED(ROT->EnumRunning(&enumMoniker))) 333 return nullptr; 334 335 win::ComPtr<IMoniker> moniker; 336 ULONG monikersFetched = 0; 337 while (SUCCEEDED(enumMoniker->Next(1, &moniker, &monikersFetched)) && monikersFetched) { 338 if (!MonikerIsVisualStudioProcess(moniker, bindCtx, dwProcessId)) 339 continue; 340 341 if (FAILED(ROT->GetObject(moniker, &punk))) 342 continue; 343 344 punk.As(&dte); 345 if (dte) 346 return dte; 347 } 348 349 return nullptr; 350} 351 352static bool HaveRunningVisualStudioOpenFile(const win::ComPtr<EnvDTE::_DTE> &dte, const std::filesystem::path &filename, int line) { 353 BStrHolder bstrFileName(filename.c_str()); 354 BStrHolder bstrKind(L"{00000000-0000-0000-0000-000000000000}"); // EnvDTE::vsViewKindPrimary 355 win::ComPtr<EnvDTE::Window> window = nullptr; 356 357 CRetryMessageFilter retryMessageFilter; 358 359 if (!filename.empty()) { 360 std::wcout << "Getting operations API from the Visual Studio session." << std::endl; 361 362 win::ComPtr<EnvDTE::ItemOperations> item_ops; 363 if (FAILED(dte->get_ItemOperations(&item_ops))) 364 return false; 365 366 std::wcout << "Waiting for the Visual Studio session to open the file: " << filename.wstring() << "." << std::endl; 367 368 if (FAILED(item_ops->OpenFile(bstrFileName, bstrKind, &window))) 369 return false; 370 371 if (line > 0) { 372 win::ComPtr<IDispatch> selection_dispatch; 373 if (window && SUCCEEDED(window->get_Selection(&selection_dispatch))) { 374 win::ComPtr<EnvDTE::TextSelection> selection; 375 if (selection_dispatch && 376 SUCCEEDED(selection_dispatch->QueryInterface(__uuidof(EnvDTE::TextSelection), &selection)) && 377 selection) { 378 selection->GotoLine(line, false); 379 selection->EndOfLine(false); 380 } 381 } 382 } 383 } 384 385 window = nullptr; 386 if (SUCCEEDED(dte->get_MainWindow(&window))) { 387 // Allow the DTE to make its main window the foreground 388 HWND hWnd; 389 window->get_HWnd((LONG *)&hWnd); 390 391 DWORD processID; 392 if (SUCCEEDED(GetWindowThreadProcessId(hWnd, &processID))) 393 AllowSetForegroundWindow(processID); 394 395 // Activate() set the window to visible and active (blinks in taskbar) 396 window->Activate(); 397 } 398 399 return true; 400} 401 402static bool VisualStudioOpenFile( 403 const std::filesystem::path &visualStudioExecutablePath, 404 const std::filesystem::path &solutionPath, 405 const std::filesystem::path &filename, 406 int line) 407{ 408 win::ComPtr<EnvDTE::_DTE> dte = nullptr; 409 410 std::wcout << "Looking for a running Visual Studio session." << std::endl; 411 412 // TODO: If path does not exist pass empty, which will just try to match all windows with solution 413 dte = FindRunningVisualStudioWithSolution(visualStudioExecutablePath, solutionPath); 414 415 if (!dte) { 416 std::wcout << "No appropriate running Visual Studio session not found, creating a new one." << std::endl; 417 418 DisplayProgressbar(); 419 420 DWORD dwProcessId; 421 if (!StartVisualStudioProcess(visualStudioExecutablePath, solutionPath, &dwProcessId)) { 422 ClearProgressbar(); 423 return false; 424 } 425 426 int timeWaited = 0; 427 428 while (timeWaited < TIMEOUT_MS) { 429 dte = FindRunningVisualStudioWithPID(dwProcessId); 430 431 if (dte) 432 break; 433 434 std::wcout << "Retrying to acquire DTE" << std::endl; 435 436 Sleep(RETRY_INTERVAL_MS); 437 timeWaited += RETRY_INTERVAL_MS; 438 } 439 440 ClearProgressbar(); 441 442 if (!dte) 443 return false; 444 } 445 else { 446 std::wcout << "Using the existing Visual Studio session." << std::endl; 447 } 448 449 return HaveRunningVisualStudioOpenFile(dte, filename, line); 450} 451 452int wmain(int argc, wchar_t* argv[]) { 453 454 // We need this to properly display UTF16 text on the console 455 _setmode(_fileno(stdout), _O_U16TEXT); 456 457 if (argc != 3 && argc != 5) { 458 std::wcerr << argc << ": wrong number of arguments\n" << "Usage: com.exe installationPath solutionPath [fileName lineNumber]" << std::endl; 459 for (int i = 0; i < argc; i++) { 460 std::wcerr << argv[i] << std::endl; 461 } 462 return EXIT_FAILURE; 463 } 464 465 if (FAILED(CoInitialize(nullptr))) { 466 std::wcerr << "CoInitialize failed." << std::endl; 467 return EXIT_FAILURE; 468 } 469 470 std::filesystem::path visualStudioExecutablePath = std::filesystem::absolute(argv[1]); 471 std::filesystem::path solutionPath = std::filesystem::absolute(argv[2]); 472 473 if (argc == 3) { 474 VisualStudioOpenFile(visualStudioExecutablePath, solutionPath, L"", -1); 475 return EXIT_SUCCESS; 476 } 477 478 std::filesystem::path fileName = std::filesystem::absolute(argv[3]); 479 int lineNumber = std::stoi(argv[4]); 480 481 VisualStudioOpenFile(visualStudioExecutablePath, solutionPath, fileName, lineNumber); 482 return EXIT_SUCCESS; 483}