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, ¤tFilter);
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}