mirror of
https://github.com/aharabada/glitchy-engine-beef.git
synced 2026-11-04 21:01:48 +00:00
Reuse running Visual Studio instances
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
#if BF_PLATFORM_WINDOWS
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using DirectX.Common;
|
||||
using GlitchyEngine;
|
||||
using GlitchyEditor.Platform.Windows.Com;
|
||||
|
||||
namespace GlitchyEditor.CodeEditors;
|
||||
|
||||
/// A request to open the script solution (or a file in it) in Visual Studio.
|
||||
/// Runs on its own worker thread so the editor never blocks on a busy or starting Visual Studio.
|
||||
class DteOpenRequest
|
||||
{
|
||||
private const int ColdStartMaxPolls = 120;
|
||||
private const int ColdStartPollIntervalMs = 500;
|
||||
|
||||
private append String _solutionPath = .();
|
||||
private append String _devenvPath = .();
|
||||
private append String _fileName = .();
|
||||
|
||||
public StringView SolutionPath
|
||||
{
|
||||
get => _solutionPath;
|
||||
set => _solutionPath.Set(value);
|
||||
}
|
||||
|
||||
public StringView DevenvPath
|
||||
{
|
||||
get => _devenvPath;
|
||||
set => _devenvPath.Set(value);
|
||||
}
|
||||
|
||||
/// Leave empty to just open the project.
|
||||
public StringView FileName
|
||||
{
|
||||
get => _fileName;
|
||||
set => _fileName.Set(value);
|
||||
}
|
||||
|
||||
public int LineNumber;
|
||||
|
||||
private static int32 sWorkerActive = 0;
|
||||
|
||||
public static void Start(DteOpenRequest ownRequest)
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref sWorkerActive, 0, 1) != 0)
|
||||
{
|
||||
Log.EngineLogger.Info("Visual Studio is already being opened, ignoring the request.");
|
||||
delete ownRequest;
|
||||
return;
|
||||
}
|
||||
|
||||
Thread workerThread = new Thread(new () =>
|
||||
{
|
||||
ownRequest.Run();
|
||||
delete ownRequest;
|
||||
|
||||
Interlocked.Exchange(ref sWorkerActive, 0);
|
||||
});
|
||||
workerThread.SetName("Visual Studio DTE worker");
|
||||
// Don't block editor shutdown on a worker that is still polling; skipping the COM cleanup
|
||||
// at process exit is harmless.
|
||||
workerThread.IsBackground = true;
|
||||
workerThread.Start(true);
|
||||
}
|
||||
|
||||
private void Run()
|
||||
{
|
||||
// The worker does its own COM initialization, the editor main thread (STA via OleInitialize)
|
||||
// stays untouched.
|
||||
if (CoInitializeEx(null, COINIT_APARTMENTTHREADED).Failed(let result))
|
||||
{
|
||||
Log.EngineLogger.Error(scope $"CoInitializeEx failed: {result} ({(uint32)result:X8})");
|
||||
return;
|
||||
}
|
||||
|
||||
defer CoUninitialize();
|
||||
|
||||
if (VisualStudioDte.FindRunningInstance(SolutionPath) case .Ok(let dte))
|
||||
{
|
||||
if (FileName.IsEmpty)
|
||||
VisualStudioDte.ActivateMainWindow(dte);
|
||||
else
|
||||
VisualStudioDte.OpenFileAtLine(dte, FileName, LineNumber).IgnoreError();
|
||||
|
||||
delete dte;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Log.EngineLogger.Info(scope $"No running Visual Studio instance has \"{SolutionPath}\" open, starting a new one.");
|
||||
|
||||
ProcessStartInfo startInfo = scope .();
|
||||
startInfo.SetFileName(DevenvPath);
|
||||
startInfo.SetArguments(scope $"\"{SolutionPath}\"");
|
||||
|
||||
if (scope SpawnedProcess().Start(startInfo) case .Err)
|
||||
{
|
||||
Log.EngineLogger.Error(scope $"Failed to start Visual Studio (\"{DevenvPath}\").");
|
||||
return;
|
||||
}
|
||||
|
||||
if (FileName.IsEmpty)
|
||||
return;
|
||||
|
||||
// Wait for the new instance to finish loading the solution (Solution.FullName stays empty
|
||||
// until then), then open the file in it.
|
||||
for (int i < ColdStartMaxPolls)
|
||||
{
|
||||
Thread.Sleep(ColdStartPollIntervalMs);
|
||||
|
||||
if (VisualStudioDte.FindRunningInstance(SolutionPath) case .Ok(let startedDte))
|
||||
{
|
||||
VisualStudioDte.OpenFileAtLine(startedDte, FileName, LineNumber).IgnoreError();
|
||||
|
||||
delete startedDte;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Log.EngineLogger.Warning(scope $"Visual Studio was started but its automation interface never became available, \"{FileName}\" was not opened automatically.");
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,236 @@
|
||||
#if BF_PLATFORM_WINDOWS
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using DirectX.Common;
|
||||
using GlitchyEngine;
|
||||
using GlitchyEditor.Platform.Windows.Com;
|
||||
using System.IO;
|
||||
|
||||
namespace GlitchyEditor.CodeEditors;
|
||||
|
||||
/// Talks to running Visual Studio instances via their EnvDTE automation objects, which are
|
||||
/// published in the Running Object Table as "!VisualStudio.DTE.<version>:<pid>".
|
||||
/// All methods must be called from a thread that is initialized for COM (CoInitializeEx).
|
||||
static class VisualStudioDte
|
||||
{
|
||||
private const String MonikerPrefix = "!VisualStudio.DTE";
|
||||
/// EnvDTE.Constants.vsViewKindCode
|
||||
private const String ViewKindCode = "{7651A701-06E5-11D1-8EBD-00A0C90F26EA}";
|
||||
|
||||
/// Searches the Running Object Table for a Visual Studio instance that has the given solution open.
|
||||
public static Result<ComDispatch> FindRunningInstance(StringView solutionPath)
|
||||
{
|
||||
IRunningObjectTable* rot = null;
|
||||
if (GetRunningObjectTable(0, out rot).Failed(var result) || rot == null)
|
||||
{
|
||||
Log.EngineLogger.Error(scope $"GetRunningObjectTable failed: {result} ({(uint32)result:X8})");
|
||||
return .Err;
|
||||
}
|
||||
|
||||
defer rot.Release();
|
||||
|
||||
IBindCtx* bindCtx = null;
|
||||
if (CreateBindCtx(0, out bindCtx).Failed(out result) || bindCtx == null)
|
||||
{
|
||||
Log.EngineLogger.Error(scope $"CreateBindCtx failed: {result} ({(uint32)result:X8})");
|
||||
return .Err;
|
||||
}
|
||||
|
||||
defer bindCtx.Release();
|
||||
|
||||
IEnumMoniker* enumMoniker = null;
|
||||
if (rot.EnumRunning(out enumMoniker).Failed(out result) || enumMoniker == null)
|
||||
{
|
||||
Log.EngineLogger.Error(scope $"IRunningObjectTable.EnumRunning failed: {result} ({(uint32)result:X8})");
|
||||
return .Err;
|
||||
}
|
||||
|
||||
defer enumMoniker.Release();
|
||||
|
||||
IMoniker* moniker = null;
|
||||
while (enumMoniker.Next(1, out moniker, ?) == .S_OK)
|
||||
{
|
||||
ComDispatch dte = GetDteFromMoniker(rot, bindCtx, moniker);
|
||||
|
||||
ReleaseAndNullify!(moniker);
|
||||
|
||||
if (dte == null)
|
||||
continue;
|
||||
|
||||
String solutionFullName = scope .();
|
||||
|
||||
if (TryGetSolutionFullName(dte, solutionFullName))
|
||||
{
|
||||
Log.EngineLogger.Trace(scope $"Found running Visual Studio instance with solution \"{solutionFullName}\".");
|
||||
|
||||
if (SolutionPathsEqual(solutionFullName, solutionPath))
|
||||
return dte;
|
||||
}
|
||||
|
||||
// No match, cleanup.
|
||||
delete dte;
|
||||
}
|
||||
|
||||
return .Err;
|
||||
}
|
||||
|
||||
/// Brings the main window of the given Visual Studio instance to the foreground.
|
||||
public static void ActivateMainWindow(ComDispatch dte)
|
||||
{
|
||||
if (!(dte.GetObjectProperty("MainWindow") case .Ok(let mainWindow)))
|
||||
{
|
||||
Log.EngineLogger.Warning("Failed to get the Visual Studio main window.");
|
||||
return;
|
||||
}
|
||||
|
||||
defer delete mainWindow;
|
||||
|
||||
if (mainWindow.InvokeMethod("Activate") case .Ok(var activateResult))
|
||||
VariantClear(&activateResult);
|
||||
|
||||
// Activate alone often isn't enough to raise a window of another process,
|
||||
// so additionally restore and foreground it via its HWND.
|
||||
if (mainWindow.GetProperty("HWnd") case .Ok(var hwndValue))
|
||||
{
|
||||
if (hwndValue.GetHwnd() case .Ok(let hwnd))
|
||||
{
|
||||
if (IsIconic(hwnd))
|
||||
ShowWindow(hwnd, SW_RESTORE);
|
||||
|
||||
SetForegroundWindow(hwnd);
|
||||
}
|
||||
|
||||
VariantClear(&hwndValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the given file in the code editor of the given Visual Studio instance and
|
||||
/// jumps to lineNumber (1-based, ignored if <= 0).
|
||||
public static Result<void> OpenFileAtLine(ComDispatch dte, StringView fileName, int lineNumber)
|
||||
{
|
||||
ComDispatch itemOperations = Try!(dte.GetObjectProperty("ItemOperations"));
|
||||
defer delete itemOperations;
|
||||
|
||||
VARIANT[] openFileArgs = scope .(VARIANT.FromBStr(fileName), VARIANT.FromBStr(ViewKindCode));
|
||||
|
||||
if (itemOperations.InvokeMethod("OpenFile", openFileArgs) case .Ok(var window))
|
||||
{
|
||||
VariantClear(&window);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.EngineLogger.Error(scope $"Failed to open \"{fileName}\" in Visual Studio.");
|
||||
return .Err;
|
||||
}
|
||||
|
||||
if (lineNumber > 0)
|
||||
GoToLine(dte, lineNumber);
|
||||
|
||||
ActivateMainWindow(dte);
|
||||
|
||||
return .Ok;
|
||||
}
|
||||
|
||||
private static ComDispatch GetDteFromMoniker(IRunningObjectTable* rot, IBindCtx* bindCtx, IMoniker* moniker)
|
||||
{
|
||||
char16* displayNameW = null;
|
||||
|
||||
if (moniker.GetDisplayName(bindCtx, null, out displayNameW).Failed || displayNameW == null)
|
||||
return null;
|
||||
|
||||
String displayName = scope .();
|
||||
displayName.Append(displayNameW);
|
||||
CoTaskMemFree(displayNameW);
|
||||
|
||||
if (!displayName.StartsWith(MonikerPrefix))
|
||||
return null;
|
||||
|
||||
IUnknown* unknown = null;
|
||||
defer { unknown?.Release(); }
|
||||
|
||||
if (rot.GetObject(moniker, out unknown).Failed || unknown == null)
|
||||
return null;
|
||||
|
||||
IDispatch* dispatch = null;
|
||||
|
||||
if (unknown.QueryInterface<IDispatch>(out dispatch).Succeeded)
|
||||
return new ComDispatch(dispatch);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool TryGetSolutionFullName(ComDispatch dte, String outFullName)
|
||||
{
|
||||
if (!(dte.GetObjectProperty("Solution") case .Ok(let solution)))
|
||||
return false;
|
||||
|
||||
defer delete solution;
|
||||
|
||||
return solution.GetStringProperty("FullName", outFullName) case .Ok;
|
||||
}
|
||||
|
||||
private static bool SolutionPathsEqual(StringView left, StringView right)
|
||||
{
|
||||
String normalizedLeft = NormalizePath(left, .. scope .());
|
||||
String normalizedRight = NormalizePath(right, .. scope .());
|
||||
|
||||
// Be tolerant about the solution extension: it isn't guaranteed that the DTE reports
|
||||
// the same extension for .slnx solutions that we expect.
|
||||
return StripSolutionExtension(normalizedLeft) == StripSolutionExtension(normalizedRight);
|
||||
}
|
||||
|
||||
private static void NormalizePath(StringView path, String output)
|
||||
{
|
||||
output.Append(path);
|
||||
output.Replace('/', '\\');
|
||||
output.ToLower();
|
||||
}
|
||||
|
||||
private static StringView StripSolutionExtension(StringView path)
|
||||
{
|
||||
if (path.EndsWith(".slnx"))
|
||||
return path.Substring(0, path.Length - ".slnx".Length);
|
||||
|
||||
if (path.EndsWith(".sln"))
|
||||
return path.Substring(0, path.Length - ".sln".Length);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private static void GoToLine(ComDispatch dte, int lineNumber)
|
||||
{
|
||||
ComDispatch document = null;
|
||||
defer { delete document; }
|
||||
|
||||
// Directly after OpenFile the ActiveDocument might not be set yet, so retry for a bit.
|
||||
for (int i < 10)
|
||||
{
|
||||
if (dte.GetObjectProperty("ActiveDocument") case .Ok(out document))
|
||||
break;
|
||||
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
if (document == null)
|
||||
{
|
||||
Log.EngineLogger.Warning("Could not jump to the requested line: no active document.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.GetObjectProperty("Selection") not case .Ok(let selection))
|
||||
{
|
||||
Log.EngineLogger.Warning("Could not jump to the requested line: failed to get the text selection.");
|
||||
return;
|
||||
}
|
||||
|
||||
VARIANT[2] gotoLineArgs = .(VARIANT.FromInt32((int32)lineNumber), VARIANT.FromBool(false));
|
||||
|
||||
if (selection.InvokeMethod("GotoLine", gotoLineArgs) case .Ok(var gotoResult))
|
||||
VariantClear(&gotoResult);
|
||||
|
||||
delete selection;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,7 +1,8 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using GlitchyEngine;
|
||||
using System.Collections;
|
||||
using GlitchyEditor.EditWindows;
|
||||
using GlitchyEditor.Settings;
|
||||
|
||||
namespace GlitchyEditor.CodeEditors;
|
||||
@@ -17,55 +18,53 @@ class VisualStudioIdeAdapter : IIdeAdapter
|
||||
_ideInstallation = ideInstallation;
|
||||
}
|
||||
|
||||
public bool IsRunning()
|
||||
{
|
||||
List<Process> processes = scope .();
|
||||
|
||||
if (Process.GetProcesses(processes) case .Err)
|
||||
{
|
||||
Log.EngineLogger.Error("Failed to get running processes.");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Process process in processes)
|
||||
{
|
||||
// TODO?
|
||||
}
|
||||
|
||||
ClearAndDeleteItems!(processes);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OpenScript(StringView fileName, int lineNumber)
|
||||
{
|
||||
if (IsRunning())
|
||||
{
|
||||
ProcessStartInfo startInfo = scope .();
|
||||
startInfo.SetFileName(_ideInstallation.Path);
|
||||
startInfo.SetArguments(scope $"/Edit {fileName}");
|
||||
|
||||
scope SpawnedProcess().Start(startInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessStartInfo startInfo = scope .();
|
||||
startInfo.SetFileName(_ideInstallation.Path);
|
||||
startInfo.SetArguments(scope $"/Edit {fileName}");
|
||||
|
||||
scope SpawnedProcess().Start(startInfo);
|
||||
}
|
||||
Open(fileName, lineNumber);
|
||||
}
|
||||
|
||||
public void OpenScriptProject()
|
||||
{
|
||||
String solutionPath = scope .();
|
||||
Open(null, 0);
|
||||
}
|
||||
|
||||
private void Open(StringView fileName, int lineNumber)
|
||||
{
|
||||
String solutionPath = scope String();
|
||||
Editor.Instance.CurrentProject.GetPathToScriptSolutionFile(solutionPath);
|
||||
|
||||
ProcessStartInfo psi = scope .();
|
||||
psi.SetFileName(_ideInstallation.Path);
|
||||
psi.SetArguments(solutionPath);
|
||||
if (!File.Exists(solutionPath))
|
||||
{
|
||||
Log.EngineLogger.Error(scope $"Could not find solution file \"{solutionPath}\".");
|
||||
return;
|
||||
}
|
||||
|
||||
scope SpawnedProcess().Start(psi);
|
||||
if (!File.Exists(_ideInstallation.Path))
|
||||
{
|
||||
Editor.Instance.ShowSettings();
|
||||
Editor.Instance.SettingsWindow.HighlightSetting("Tools", "IDE");
|
||||
|
||||
PopupService.Instance.ShowMessageBox("Visual Studio not found.",
|
||||
"The Visual Studio path could not be found. Please select a different IDE.");
|
||||
return;
|
||||
}
|
||||
|
||||
#if BF_PLATFORM_WINDOWS
|
||||
// Reuses a running Visual Studio instance that has our solution open (via EnvDTE),
|
||||
// otherwise starts a new one.
|
||||
DteOpenRequest request = new .();
|
||||
request.SolutionPath = solutionPath;
|
||||
request.DevenvPath = _ideInstallation.Path;
|
||||
request.FileName = fileName;
|
||||
request.LineNumber = lineNumber;
|
||||
|
||||
DteOpenRequest.Start(request);
|
||||
#else
|
||||
ProcessStartInfo startInfo = scope .();
|
||||
startInfo.SetFileName(_ideInstallation.Path);
|
||||
startInfo.SetArguments(scope $"\"{solutionPath}\"");
|
||||
|
||||
scope SpawnedProcess().Start(startInfo);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user