123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483 |
- /*---------------------------------------------------------------------------------------------
- * Copyright (c) Unity Technologies.
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
-
- #include <iostream>
- #include <sstream>
- #include <string>
- #include <filesystem>
- #include <windows.h>
- #include <shlwapi.h>
-
- #include <fcntl.h>
- #include <io.h>
-
- #include "BStrHolder.h"
- #include "ComPtr.h"
- #include "dte80a.tlh"
-
- constexpr int RETRY_INTERVAL_MS = 150;
- constexpr int TIMEOUT_MS = 10000;
-
- // Often a DTE call made to Visual Studio can fail after Visual Studio has just started. Usually the
- // return value will be RPC_E_CALL_REJECTED, meaning that Visual Studio is probably busy on another
- // thread. This types filter the RPC messages and retries to send the message until VS accepts it.
- class CRetryMessageFilter : public IMessageFilter
- {
- private:
- static bool ShouldRetryCall(DWORD dwTickCount, DWORD dwRejectType)
- {
- if (dwRejectType == SERVERCALL_RETRYLATER || dwRejectType == SERVERCALL_REJECTED) {
- return dwTickCount < TIMEOUT_MS;
- }
-
- return false;
- }
-
- win::ComPtr<IMessageFilter> currentFilter;
-
- public:
- CRetryMessageFilter()
- {
- HRESULT hr = CoRegisterMessageFilter(this, ¤tFilter);
- _ASSERT(SUCCEEDED(hr));
- }
-
- ~CRetryMessageFilter()
- {
- win::ComPtr<IMessageFilter> messageFilter;
- HRESULT hr = CoRegisterMessageFilter(currentFilter, &messageFilter);
- _ASSERT(SUCCEEDED(hr));
- }
-
- // IUnknown methods
- IFACEMETHODIMP QueryInterface(REFIID riid, void** ppv)
- {
- static const QITAB qit[] =
- {
- QITABENT(CRetryMessageFilter, IMessageFilter),
- { 0 },
- };
- return QISearch(this, qit, riid, ppv);
- }
-
- IFACEMETHODIMP_(ULONG) AddRef()
- {
- return 0;
- }
-
- IFACEMETHODIMP_(ULONG) Release()
- {
- return 0;
- }
-
- DWORD STDMETHODCALLTYPE HandleInComingCall(DWORD dwCallType, HTASK htaskCaller, DWORD dwTickCount, LPINTERFACEINFO lpInterfaceInfo)
- {
- if (currentFilter)
- return currentFilter->HandleInComingCall(dwCallType, htaskCaller, dwTickCount, lpInterfaceInfo);
-
- return SERVERCALL_ISHANDLED;
- }
-
- DWORD STDMETHODCALLTYPE RetryRejectedCall(HTASK htaskCallee, DWORD dwTickCount, DWORD dwRejectType)
- {
- if (ShouldRetryCall(dwTickCount, dwRejectType))
- return RETRY_INTERVAL_MS;
-
- if (currentFilter)
- return currentFilter->RetryRejectedCall(htaskCallee, dwTickCount, dwRejectType);
-
- return (DWORD)-1;
- }
-
- DWORD STDMETHODCALLTYPE MessagePending(HTASK htaskCallee, DWORD dwTickCount, DWORD dwPendingType)
- {
- if (currentFilter)
- return currentFilter->MessagePending(htaskCallee, dwTickCount, dwPendingType);
-
- return PENDINGMSG_WAITDEFPROCESS;
- }
- };
-
- static void DisplayProgressbar() {
- std::wcout << "displayProgressBar" << std::endl;
- }
-
- static void ClearProgressbar() {
- std::wcout << "clearprogressbar" << std::endl;
- }
-
- inline const std::wstring QuoteString(const std::wstring& str)
- {
- return L"\"" + str + L"\"";
- }
-
- static std::wstring ErrorCodeToMsg(DWORD code)
- {
- LPWSTR msgBuf = nullptr;
- if (!FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
- nullptr, code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)&msgBuf, 0, nullptr))
- {
- return L"Unknown error";
- }
- else
- {
- return msgBuf;
- }
- }
-
- // Get an environment variable
- static std::wstring GetEnvironmentVariableValue(const std::wstring& variableName) {
- DWORD currentBufferSize = MAX_PATH;
- std::wstring variableValue;
- variableValue.resize(currentBufferSize);
-
- DWORD requiredBufferSize = GetEnvironmentVariableW(variableName.c_str(), variableValue.data(), currentBufferSize);
- if (requiredBufferSize == 0) {
- // Environment variable probably does not exist.
- return std::wstring();
- }
-
- if (currentBufferSize < requiredBufferSize) {
- variableValue.resize(requiredBufferSize);
- if (GetEnvironmentVariableW(variableName.c_str(), variableValue.data(), currentBufferSize) == 0)
- return std::wstring();
- }
-
- variableValue.resize(requiredBufferSize);
- return variableValue;
- }
-
- static bool StartVisualStudioProcess(
- const std::filesystem::path &visualStudioExecutablePath,
- const std::filesystem::path &solutionPath,
- DWORD *dwProcessId) {
-
- STARTUPINFOW si;
- PROCESS_INFORMATION pi;
- BOOL result;
-
- ZeroMemory(&si, sizeof(si));
- si.cb = sizeof(si);
- ZeroMemory(&pi, sizeof(pi));
-
- std::wstring startingDirectory = visualStudioExecutablePath.parent_path();
-
- // Build the command line that is passed as the argv of the VS process
- // argv[0] must be the quoted full path to the VS exe
- std::wstringstream commandLineStream;
- commandLineStream << QuoteString(visualStudioExecutablePath) << L" ";
-
- std::wstring vsArgsWide = GetEnvironmentVariableValue(L"UNITY_VS_ARGS");
- if (!vsArgsWide.empty())
- commandLineStream << vsArgsWide << L" ";
-
- commandLineStream << QuoteString(solutionPath);
-
- std::wstring commandLine = commandLineStream.str();
-
- std::wcout << "Starting Visual Studio process with: " << commandLine << std::endl;
-
- result = CreateProcessW(
- visualStudioExecutablePath.c_str(), // Full path to VS, must not be quoted
- commandLine.data(), // Command line, as passed as argv, separate arguments must be quoted if they contain spaces
- nullptr, // Process handle not inheritable
- nullptr, // Thread handle not inheritable
- false, // Set handle inheritance to FALSE
- 0, // No creation flags
- nullptr, // Use parent's environment block
- startingDirectory.c_str(), // starting directory set to the VS directory
- &si,
- &pi);
-
- if (!result) {
- DWORD error = GetLastError();
- std::wcout << "Starting Visual Studio process failed: " << ErrorCodeToMsg(error) << std::endl;
- return false;
- }
-
- *dwProcessId = pi.dwProcessId;
- CloseHandle(pi.hProcess);
- CloseHandle(pi.hThread);
-
- return true;
- }
-
- static bool
- MonikerIsVisualStudioProcess(const win::ComPtr<IMoniker> &moniker, const win::ComPtr<IBindCtx> &bindCtx, const DWORD dwProcessId = 0) {
- LPOLESTR oleMonikerName;
- if (FAILED(moniker->GetDisplayName(bindCtx, nullptr, &oleMonikerName)))
- return false;
-
- std::wstring monikerName(oleMonikerName);
-
- // VisualStudio Moniker is "!VisualStudio.DTE.$Version:$PID"
- // Example "!VisualStudio.DTE.14.0:1234"
-
- if (monikerName.find(L"!VisualStudio.DTE") != 0)
- return false;
-
- if (dwProcessId == 0)
- return true;
-
- std::wstringstream suffixStream;
- suffixStream << ":";
- suffixStream << dwProcessId;
-
- std::wstring suffix(suffixStream.str());
-
- return monikerName.length() - suffix.length() == monikerName.find(suffix);
- }
-
- static win::ComPtr<EnvDTE::_DTE> FindRunningVisualStudioWithSolution(
- const std::filesystem::path &visualStudioExecutablePath,
- const std::filesystem::path &solutionPath)
- {
- win::ComPtr<IUnknown> punk = nullptr;
- win::ComPtr<EnvDTE::_DTE> dte = nullptr;
-
- CRetryMessageFilter retryMessageFilter;
-
- // Search through the Running Object Table for an instance of Visual Studio
- // to use that either has the correct solution already open or does not have
- // any solution open.
- win::ComPtr<IRunningObjectTable> ROT;
- if (FAILED(GetRunningObjectTable(0, &ROT)))
- return nullptr;
-
- win::ComPtr<IBindCtx> bindCtx;
- if (FAILED(CreateBindCtx(0, &bindCtx)))
- return nullptr;
-
- win::ComPtr<IEnumMoniker> enumMoniker;
- if (FAILED(ROT->EnumRunning(&enumMoniker)))
- return nullptr;
-
- win::ComPtr<IMoniker> moniker;
- ULONG monikersFetched = 0;
- while (SUCCEEDED(enumMoniker->Next(1, &moniker, &monikersFetched)) && monikersFetched) {
- if (!MonikerIsVisualStudioProcess(moniker, bindCtx))
- continue;
-
- if (FAILED(ROT->GetObject(moniker, &punk)))
- continue;
-
- punk.As(&dte);
- if (!dte)
- continue;
-
- // Okay, so we found an actual running instance of Visual Studio.
-
- // Get the executable path of this running instance.
- BStrHolder visualStudioFullName;
- if (FAILED(dte->get_FullName(&visualStudioFullName)))
- continue;
-
- std::filesystem::path currentVisualStudioExecutablePath = std::wstring(visualStudioFullName);
-
- // Ask for its current solution.
- win::ComPtr<EnvDTE::_Solution> solution;
- if (FAILED(dte->get_Solution(&solution)))
- continue;
-
- // Get the name of that solution.
- BStrHolder solutionFullName;
- if (FAILED(solution->get_FullName(&solutionFullName)))
- continue;
-
- std::filesystem::path currentSolutionPath = std::wstring(solutionFullName);
- if (currentSolutionPath.empty())
- continue;
-
- std::wcout << "Visual Studio opened on " << currentSolutionPath.wstring() << std::endl;
-
- // 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.
- // If we don't have a Visual Studio installation path to use, just use this solution.
- if (std::filesystem::equivalent(currentSolutionPath, solutionPath)) {
- std::wcout << "We found a running Visual Studio session with the solution open." << std::endl;
- if (!visualStudioExecutablePath.empty()) {
- if (std::filesystem::equivalent(currentVisualStudioExecutablePath, visualStudioExecutablePath)) {
- return dte;
- }
- else {
- 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;
- }
- }
- else {
- 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;
- return dte;
- }
- }
- }
- return nullptr;
- }
-
- static win::ComPtr<EnvDTE::_DTE> FindRunningVisualStudioWithPID(const DWORD dwProcessId) {
- win::ComPtr<IUnknown> punk = nullptr;
- win::ComPtr<EnvDTE::_DTE> dte = nullptr;
-
- // Search through the Running Object Table for a Visual Studio
- // process with the process ID specified
- win::ComPtr<IRunningObjectTable> ROT;
- if (FAILED(GetRunningObjectTable(0, &ROT)))
- return nullptr;
-
- win::ComPtr<IBindCtx> bindCtx;
- if (FAILED(CreateBindCtx(0, &bindCtx)))
- return nullptr;
-
- win::ComPtr<IEnumMoniker> enumMoniker;
- if (FAILED(ROT->EnumRunning(&enumMoniker)))
- return nullptr;
-
- win::ComPtr<IMoniker> moniker;
- ULONG monikersFetched = 0;
- while (SUCCEEDED(enumMoniker->Next(1, &moniker, &monikersFetched)) && monikersFetched) {
- if (!MonikerIsVisualStudioProcess(moniker, bindCtx, dwProcessId))
- continue;
-
- if (FAILED(ROT->GetObject(moniker, &punk)))
- continue;
-
- punk.As(&dte);
- if (dte)
- return dte;
- }
-
- return nullptr;
- }
-
- static bool HaveRunningVisualStudioOpenFile(const win::ComPtr<EnvDTE::_DTE> &dte, const std::filesystem::path &filename, int line) {
- BStrHolder bstrFileName(filename.c_str());
- BStrHolder bstrKind(L"{00000000-0000-0000-0000-000000000000}"); // EnvDTE::vsViewKindPrimary
- win::ComPtr<EnvDTE::Window> window = nullptr;
-
- CRetryMessageFilter retryMessageFilter;
-
- if (!filename.empty()) {
- std::wcout << "Getting operations API from the Visual Studio session." << std::endl;
-
- win::ComPtr<EnvDTE::ItemOperations> item_ops;
- if (FAILED(dte->get_ItemOperations(&item_ops)))
- return false;
-
- std::wcout << "Waiting for the Visual Studio session to open the file: " << filename.wstring() << "." << std::endl;
-
- if (FAILED(item_ops->OpenFile(bstrFileName, bstrKind, &window)))
- return false;
-
- if (line > 0) {
- win::ComPtr<IDispatch> selection_dispatch;
- if (window && SUCCEEDED(window->get_Selection(&selection_dispatch))) {
- win::ComPtr<EnvDTE::TextSelection> selection;
- if (selection_dispatch &&
- SUCCEEDED(selection_dispatch->QueryInterface(__uuidof(EnvDTE::TextSelection), &selection)) &&
- selection) {
- selection->GotoLine(line, false);
- selection->EndOfLine(false);
- }
- }
- }
- }
-
- window = nullptr;
- if (SUCCEEDED(dte->get_MainWindow(&window))) {
- // Allow the DTE to make its main window the foreground
- HWND hWnd;
- window->get_HWnd((LONG *)&hWnd);
-
- DWORD processID;
- if (SUCCEEDED(GetWindowThreadProcessId(hWnd, &processID)))
- AllowSetForegroundWindow(processID);
-
- // Activate() set the window to visible and active (blinks in taskbar)
- window->Activate();
- }
-
- return true;
- }
-
- static bool VisualStudioOpenFile(
- const std::filesystem::path &visualStudioExecutablePath,
- const std::filesystem::path &solutionPath,
- const std::filesystem::path &filename,
- int line)
- {
- win::ComPtr<EnvDTE::_DTE> dte = nullptr;
-
- std::wcout << "Looking for a running Visual Studio session." << std::endl;
-
- // TODO: If path does not exist pass empty, which will just try to match all windows with solution
- dte = FindRunningVisualStudioWithSolution(visualStudioExecutablePath, solutionPath);
-
- if (!dte) {
- std::wcout << "No appropriate running Visual Studio session not found, creating a new one." << std::endl;
-
- DisplayProgressbar();
-
- DWORD dwProcessId;
- if (!StartVisualStudioProcess(visualStudioExecutablePath, solutionPath, &dwProcessId)) {
- ClearProgressbar();
- return false;
- }
-
- int timeWaited = 0;
-
- while (timeWaited < TIMEOUT_MS) {
- dte = FindRunningVisualStudioWithPID(dwProcessId);
-
- if (dte)
- break;
-
- std::wcout << "Retrying to acquire DTE" << std::endl;
-
- Sleep(RETRY_INTERVAL_MS);
- timeWaited += RETRY_INTERVAL_MS;
- }
-
- ClearProgressbar();
-
- if (!dte)
- return false;
- }
- else {
- std::wcout << "Using the existing Visual Studio session." << std::endl;
- }
-
- return HaveRunningVisualStudioOpenFile(dte, filename, line);
- }
-
- int wmain(int argc, wchar_t* argv[]) {
-
- // We need this to properly display UTF16 text on the console
- _setmode(_fileno(stdout), _O_U16TEXT);
-
- if (argc != 3 && argc != 5) {
- std::wcerr << argc << ": wrong number of arguments\n" << "Usage: com.exe installationPath solutionPath [fileName lineNumber]" << std::endl;
- for (int i = 0; i < argc; i++) {
- std::wcerr << argv[i] << std::endl;
- }
- return EXIT_FAILURE;
- }
-
- if (FAILED(CoInitialize(nullptr))) {
- std::wcerr << "CoInitialize failed." << std::endl;
- return EXIT_FAILURE;
- }
-
- std::filesystem::path visualStudioExecutablePath = std::filesystem::absolute(argv[1]);
- std::filesystem::path solutionPath = std::filesystem::absolute(argv[2]);
-
- if (argc == 3) {
- VisualStudioOpenFile(visualStudioExecutablePath, solutionPath, L"", -1);
- return EXIT_SUCCESS;
- }
-
- std::filesystem::path fileName = std::filesystem::absolute(argv[3]);
- int lineNumber = std::stoi(argv[4]);
-
- VisualStudioOpenFile(visualStudioExecutablePath, solutionPath, fileName, lineNumber);
- return EXIT_SUCCESS;
- }
|