You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
483 lines
14 KiB
483 lines
14 KiB
/*--------------------------------------------------------------------------------------------- |
|
* 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; |
|
}
|
|
|