COMIntegration.cpp 14 KB

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