mirror of
https://github.com/aharabada/glitchy-engine-beef.git
synced 2026-09-05 13:01:52 +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;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
using GlitchyEngine;
|
using GlitchyEngine;
|
||||||
using System.Collections;
|
using GlitchyEditor.EditWindows;
|
||||||
using GlitchyEditor.Settings;
|
using GlitchyEditor.Settings;
|
||||||
|
|
||||||
namespace GlitchyEditor.CodeEditors;
|
namespace GlitchyEditor.CodeEditors;
|
||||||
@@ -17,55 +18,53 @@ class VisualStudioIdeAdapter : IIdeAdapter
|
|||||||
_ideInstallation = ideInstallation;
|
_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)
|
public void OpenScript(StringView fileName, int lineNumber)
|
||||||
{
|
{
|
||||||
if (IsRunning())
|
Open(fileName, lineNumber);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OpenScriptProject()
|
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);
|
Editor.Instance.CurrentProject.GetPathToScriptSolutionFile(solutionPath);
|
||||||
|
|
||||||
ProcessStartInfo psi = scope .();
|
if (!File.Exists(solutionPath))
|
||||||
psi.SetFileName(_ideInstallation.Path);
|
{
|
||||||
psi.SetArguments(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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
#if BF_PLATFORM_WINDOWS
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using DirectX.Common;
|
||||||
|
using GlitchyEngine;
|
||||||
|
|
||||||
|
namespace GlitchyEditor.Platform.Windows.Com;
|
||||||
|
|
||||||
|
/// Owns an IDispatch pointer and provides late bound access to its properties and methods
|
||||||
|
/// (GetIDsOfNames + Invoke). Calls that are rejected because the COM server is busy
|
||||||
|
/// (e.g. Visual Studio showing a modal dialog or building) are retried for a bounded time.
|
||||||
|
class ComDispatch
|
||||||
|
{
|
||||||
|
private const int MaxBusyRetries = 20;
|
||||||
|
private const int BusyRetrySleepMs = 150;
|
||||||
|
|
||||||
|
private static Guid sIID_NULL = .();
|
||||||
|
|
||||||
|
private IDispatch* _dispatch;
|
||||||
|
|
||||||
|
/// Takes ownership of the reference held by dispatch.
|
||||||
|
public this(IDispatch* dispatch)
|
||||||
|
{
|
||||||
|
_dispatch = dispatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ~this()
|
||||||
|
{
|
||||||
|
if (_dispatch != null)
|
||||||
|
_dispatch.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The caller must release the returned VARIANT with VariantClear.
|
||||||
|
public Result<VARIANT> GetProperty(StringView name)
|
||||||
|
{
|
||||||
|
return InvokeInternal(name, .PropertyGet, default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arguments are passed in natural (declaration) order and are released by this method.
|
||||||
|
/// The caller must release the returned VARIANT with VariantClear.
|
||||||
|
public Result<VARIANT> InvokeMethod(StringView name, Span<VARIANT> args = default)
|
||||||
|
{
|
||||||
|
Result<VARIANT> result = InvokeInternal(name, .Method, args);
|
||||||
|
|
||||||
|
for (int i < args.Length)
|
||||||
|
VariantClear(&args[i]);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the property value as a new ComDispatch (fails if the property is not an object or null).
|
||||||
|
public Result<ComDispatch> GetObjectProperty(StringView name)
|
||||||
|
{
|
||||||
|
VARIANT value = Try!(GetProperty(name));
|
||||||
|
|
||||||
|
if (value.vt == .Dispatch && value.pdispVal != null)
|
||||||
|
{
|
||||||
|
// The new ComDispatch takes over the reference held by the VARIANT, so no VariantClear here.
|
||||||
|
return new ComDispatch(value.pdispVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
VariantClear(&value);
|
||||||
|
return .Err;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Appends the string property value to outValue.
|
||||||
|
public Result<void> GetStringProperty(StringView name, String outValue)
|
||||||
|
{
|
||||||
|
VARIANT value = Try!(GetProperty(name));
|
||||||
|
defer VariantClear(&value);
|
||||||
|
|
||||||
|
if (value.vt != .Bstr || value.bstrVal == null)
|
||||||
|
return .Err;
|
||||||
|
|
||||||
|
outValue.Append(value.bstrVal);
|
||||||
|
return .Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result<VARIANT> InvokeInternal(StringView name, DispatchFlags flags, Span<VARIANT> args)
|
||||||
|
{
|
||||||
|
char16* nameW = name.ToScopedNativeWChar!();
|
||||||
|
|
||||||
|
int32 dispId = 0;
|
||||||
|
HResult result = _dispatch.GetIDsOfNames(&sIID_NULL, &nameW, 1, LOCALE_USER_DEFAULT, &dispId);
|
||||||
|
|
||||||
|
if (result.Failed)
|
||||||
|
{
|
||||||
|
Log.EngineLogger.Error(scope $"IDispatch.GetIDsOfNames failed for \"{name}\": {result} ({(uint32)result:X8})");
|
||||||
|
return .Err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDispatch expects the arguments in reverse order (rgvarg[0] is the last parameter).
|
||||||
|
VARIANT[] reversedArgs = scope VARIANT[args.Length];
|
||||||
|
|
||||||
|
for (int i < args.Length)
|
||||||
|
reversedArgs[i] = args[args.Length - 1 - i];
|
||||||
|
|
||||||
|
DISPPARAMS dispParams = .()
|
||||||
|
{
|
||||||
|
rgvarg = reversedArgs.Ptr,
|
||||||
|
rgdispidNamedArgs = null,
|
||||||
|
cArgs = (uint32)args.Length,
|
||||||
|
cNamedArgs = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
VARIANT returnValue = default;
|
||||||
|
EXCEPINFO excepInfo = default;
|
||||||
|
|
||||||
|
for (int attempt = 0; true; attempt++)
|
||||||
|
{
|
||||||
|
result = _dispatch.Invoke(dispId, &sIID_NULL, LOCALE_USER_DEFAULT, flags, &dispParams, &returnValue, &excepInfo, null);
|
||||||
|
|
||||||
|
if ((result == .RPC_E_CALL_REJECTED || result == .RPC_E_SERVERCALL_RETRYLATER) && attempt < MaxBusyRetries)
|
||||||
|
{
|
||||||
|
Thread.Sleep(BusyRetrySleepMs);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == .DISP_E_EXCEPTION)
|
||||||
|
{
|
||||||
|
if (excepInfo.bstrDescription == null && excepInfo.pfnDeferredFillIn != null)
|
||||||
|
excepInfo.pfnDeferredFillIn(&excepInfo);
|
||||||
|
|
||||||
|
String description = scope .();
|
||||||
|
|
||||||
|
if (excepInfo.bstrDescription != null)
|
||||||
|
description.Append(excepInfo.bstrDescription);
|
||||||
|
|
||||||
|
Log.EngineLogger.Error(scope $"IDispatch.Invoke of \"{name}\" threw an exception: \"{description}\" (scode: {(uint32)excepInfo.scode:X8})");
|
||||||
|
|
||||||
|
excepInfo.FreeStrings();
|
||||||
|
|
||||||
|
return .Err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Failed)
|
||||||
|
{
|
||||||
|
Log.EngineLogger.Error(scope $"IDispatch.Invoke of \"{name}\" failed: {result} ({(uint32)result:X8})");
|
||||||
|
return .Err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
#if BF_PLATFORM_WINDOWS
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using DirectX.Common;
|
||||||
|
|
||||||
|
namespace GlitchyEditor.Platform.Windows.Com;
|
||||||
|
|
||||||
|
typealias BSTR = char16*;
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
public const uint32 COINIT_APARTMENTTHREADED = 0x2;
|
||||||
|
public const uint32 LOCALE_USER_DEFAULT = 0x0400;
|
||||||
|
|
||||||
|
public const int32 SW_RESTORE = 9;
|
||||||
|
|
||||||
|
[Import("Ole32.lib"), CLink, CallingConvention(.Stdcall)]
|
||||||
|
public static extern HResult CoInitializeEx(void* reserved, uint32 coInit);
|
||||||
|
[Import("Ole32.lib"), CLink, CallingConvention(.Stdcall)]
|
||||||
|
public static extern void CoUninitialize();
|
||||||
|
[Import("Ole32.lib"), CLink, CallingConvention(.Stdcall)]
|
||||||
|
public static extern void CoTaskMemFree(void* pointer);
|
||||||
|
|
||||||
|
[Import("OleAut32.lib"), CLink, CallingConvention(.Stdcall)]
|
||||||
|
public static extern BSTR SysAllocString(char16* str);
|
||||||
|
[Import("OleAut32.lib"), CLink, CallingConvention(.Stdcall)]
|
||||||
|
public static extern void SysFreeString(BSTR str);
|
||||||
|
[Import("OleAut32.lib"), CLink, CallingConvention(.Stdcall)]
|
||||||
|
public static extern uint32 SysStringLen(BSTR str);
|
||||||
|
[Import("OleAut32.lib"), CLink, CallingConvention(.Stdcall)]
|
||||||
|
public static extern HResult VariantClear(VARIANT* variant);
|
||||||
|
|
||||||
|
[Import("user32.lib"), CLink, CallingConvention(.Stdcall)]
|
||||||
|
public static extern System.Windows.IntBool SetForegroundWindow(int hwnd);
|
||||||
|
[Import("user32.lib"), CLink, CallingConvention(.Stdcall)]
|
||||||
|
public static extern System.Windows.IntBool IsIconic(int hwnd);
|
||||||
|
[Import("user32.lib"), CLink, CallingConvention(.Stdcall)]
|
||||||
|
public static extern System.Windows.IntBool ShowWindow(int hwnd, int32 cmdShow);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum VarType : uint16
|
||||||
|
{
|
||||||
|
Empty = 0,
|
||||||
|
I2 = 2,
|
||||||
|
I4 = 3,
|
||||||
|
Bstr = 8,
|
||||||
|
Dispatch = 9,
|
||||||
|
Bool = 11,
|
||||||
|
Unknown = 13,
|
||||||
|
UI4 = 19,
|
||||||
|
I8 = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DispatchFlags : uint16
|
||||||
|
{
|
||||||
|
Method = 0x1,
|
||||||
|
PropertyGet = 0x2,
|
||||||
|
PropertyPut = 0x4,
|
||||||
|
PropertyPutRef = 0x8
|
||||||
|
}
|
||||||
|
|
||||||
|
[CRepr]
|
||||||
|
public struct VARIANT
|
||||||
|
{
|
||||||
|
public const int16 VARIANT_TRUE = -1;
|
||||||
|
public const int16 VARIANT_FALSE = 0;
|
||||||
|
|
||||||
|
public VarType vt;
|
||||||
|
public uint16 wReserved1;
|
||||||
|
public uint16 wReserved2;
|
||||||
|
public uint16 wReserved3;
|
||||||
|
public using _Value Value;
|
||||||
|
|
||||||
|
[CRepr, Union]
|
||||||
|
public struct _Value
|
||||||
|
{
|
||||||
|
public int64 llVal;
|
||||||
|
public int32 lVal;
|
||||||
|
public int16 iVal;
|
||||||
|
public int16 boolVal;
|
||||||
|
public BSTR bstrVal;
|
||||||
|
public IDispatch* pdispVal;
|
||||||
|
public IUnknown* punkVal;
|
||||||
|
// BRECORD is the widest union member and forces the correct union size of 16 bytes.
|
||||||
|
public _Record brecord;
|
||||||
|
|
||||||
|
[CRepr]
|
||||||
|
public struct _Record
|
||||||
|
{
|
||||||
|
public void* pvRecord;
|
||||||
|
public void* pRecInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The returned VARIANT owns the BSTR; release it with VariantClear.
|
||||||
|
public static VARIANT FromBStr(StringView value)
|
||||||
|
{
|
||||||
|
VARIANT variant = default;
|
||||||
|
variant.vt = .Bstr;
|
||||||
|
variant.bstrVal = SysAllocString(value.ToScopedNativeWChar!());
|
||||||
|
return variant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static VARIANT FromInt32(int32 value)
|
||||||
|
{
|
||||||
|
VARIANT variant = default;
|
||||||
|
variant.vt = .I4;
|
||||||
|
variant.lVal = value;
|
||||||
|
return variant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static VARIANT FromBool(bool value)
|
||||||
|
{
|
||||||
|
VARIANT variant = default;
|
||||||
|
variant.vt = .Bool;
|
||||||
|
variant.boolVal = value ? VARIANT_TRUE : VARIANT_FALSE;
|
||||||
|
return variant;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts a window handle. VS is a 64 bit process but EnvDTE declares HWnd as a 32 bit int,
|
||||||
|
/// so depending on the marshalling we might see I4, UI4 or I8 (HWND values are 32 bit significant).
|
||||||
|
public Result<int> GetHwnd()
|
||||||
|
{
|
||||||
|
switch (vt)
|
||||||
|
{
|
||||||
|
case .I4:
|
||||||
|
return (int)lVal;
|
||||||
|
case .UI4:
|
||||||
|
return (int)(uint32)lVal;
|
||||||
|
case .I8:
|
||||||
|
return (int)llVal;
|
||||||
|
default:
|
||||||
|
return .Err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[CRepr]
|
||||||
|
public struct DISPPARAMS
|
||||||
|
{
|
||||||
|
public VARIANT* rgvarg;
|
||||||
|
public int32* rgdispidNamedArgs;
|
||||||
|
public uint32 cArgs;
|
||||||
|
public uint32 cNamedArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
[CRepr]
|
||||||
|
public struct EXCEPINFO
|
||||||
|
{
|
||||||
|
public uint16 wCode;
|
||||||
|
public uint16 wReserved;
|
||||||
|
public BSTR bstrSource;
|
||||||
|
public BSTR bstrDescription;
|
||||||
|
public BSTR bstrHelpFile;
|
||||||
|
public uint32 dwHelpContext;
|
||||||
|
public void* pvReserved;
|
||||||
|
public function [CallingConvention(.Stdcall)] HResult(EXCEPINFO* excepInfo) pfnDeferredFillIn;
|
||||||
|
public int32 scode;
|
||||||
|
|
||||||
|
public void FreeStrings() mut
|
||||||
|
{
|
||||||
|
if (bstrSource != null)
|
||||||
|
SysFreeString(bstrSource);
|
||||||
|
if (bstrDescription != null)
|
||||||
|
SysFreeString(bstrDescription);
|
||||||
|
if (bstrHelpFile != null)
|
||||||
|
SysFreeString(bstrHelpFile);
|
||||||
|
|
||||||
|
bstrSource = null;
|
||||||
|
bstrDescription = null;
|
||||||
|
bstrHelpFile = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[CRepr]
|
||||||
|
public struct IDispatch : IUnknown
|
||||||
|
{
|
||||||
|
public const new Guid IID = .(0x00020400, 0x0000, 0x0000, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46);
|
||||||
|
|
||||||
|
public new VTable* VT { get => (.)mVT; }
|
||||||
|
|
||||||
|
public HResult GetIDsOfNames(Guid* riid, char16** names, uint32 nameCount, uint32 localeId, int32* dispIds) mut =>
|
||||||
|
VT.GetIDsOfNames(&this, riid, names, nameCount, localeId, dispIds);
|
||||||
|
public HResult Invoke(int32 dispIdMember, Guid* riid, uint32 localeId, DispatchFlags flags, DISPPARAMS* dispParams, VARIANT* result, EXCEPINFO* excepInfo, uint32* argErr) mut =>
|
||||||
|
VT.Invoke(&this, dispIdMember, riid, localeId, flags, dispParams, result, excepInfo, argErr);
|
||||||
|
|
||||||
|
[CRepr]
|
||||||
|
public struct VTable : IUnknown.VTable
|
||||||
|
{
|
||||||
|
// Slots we don't use, kept as placeholders so the layout stays correct.
|
||||||
|
public void* GetTypeInfoCount;
|
||||||
|
public void* GetTypeInfo;
|
||||||
|
public function [CallingConvention(.Stdcall)] HResult(IDispatch* self, Guid* riid, char16** names, uint32 nameCount, uint32 localeId, int32* dispIds) GetIDsOfNames;
|
||||||
|
public function [CallingConvention(.Stdcall)] HResult(IDispatch* self, int32 dispIdMember, Guid* riid, uint32 localeId, DispatchFlags flags, DISPPARAMS* dispParams, VARIANT* result, EXCEPINFO* excepInfo, uint32* argErr) Invoke;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace DirectX.Common
|
||||||
|
{
|
||||||
|
extension HResult
|
||||||
|
{
|
||||||
|
/// The COM server rejected the call (e.g. Visual Studio is busy). Retry later.
|
||||||
|
public const HResult RPC_E_CALL_REJECTED = (.)0x80010001;
|
||||||
|
/// The COM server asked us to retry the call later.
|
||||||
|
public const HResult RPC_E_SERVERCALL_RETRYLATER = (.)0x8001010A;
|
||||||
|
/// IDispatch::Invoke failed with an exception, details are in the EXCEPINFO.
|
||||||
|
public const HResult DISP_E_EXCEPTION = (.)0x80020009;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
#if BF_PLATFORM_WINDOWS
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using DirectX.Common;
|
||||||
|
|
||||||
|
namespace GlitchyEditor.Platform.Windows.Com;
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
[Import("Ole32.lib"), CLink, CallingConvention(.Stdcall)]
|
||||||
|
public static extern HResult GetRunningObjectTable(uint32 reserved, out IRunningObjectTable* runningObjectTable);
|
||||||
|
[Import("Ole32.lib"), CLink, CallingConvention(.Stdcall)]
|
||||||
|
public static extern HResult CreateBindCtx(uint32 reserved, out IBindCtx* bindCtx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opaque, only ever passed along as a pointer (e.g. to IMoniker.GetDisplayName).
|
||||||
|
[CRepr]
|
||||||
|
public struct IBindCtx : IUnknown
|
||||||
|
{
|
||||||
|
public const new Guid IID = .(0x0000000e, 0x0000, 0x0000, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46);
|
||||||
|
}
|
||||||
|
|
||||||
|
[CRepr]
|
||||||
|
public struct IMoniker : IUnknown
|
||||||
|
{
|
||||||
|
public const new Guid IID = .(0x0000000f, 0x0000, 0x0000, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46);
|
||||||
|
|
||||||
|
public new VTable* VT { get => (.)mVT; }
|
||||||
|
|
||||||
|
/// The returned display name must be freed with CoTaskMemFree.
|
||||||
|
public HResult GetDisplayName(IBindCtx* bindCtx, IMoniker* monikerToLeft, out char16* displayName) mut =>
|
||||||
|
VT.GetDisplayName(&this, bindCtx, monikerToLeft, out displayName);
|
||||||
|
|
||||||
|
[CRepr]
|
||||||
|
public struct VTable : IUnknown.VTable
|
||||||
|
{
|
||||||
|
// IMoniker inherits IPersistStream (which inherits IPersist). We only need GetDisplayName,
|
||||||
|
// all other slots are placeholders that keep the layout correct.
|
||||||
|
public void* GetClassID; // IPersist
|
||||||
|
public void* IsDirty; // IPersistStream
|
||||||
|
public void* Load;
|
||||||
|
public void* Save;
|
||||||
|
public void* GetSizeMax;
|
||||||
|
public void* BindToObject; // IMoniker
|
||||||
|
public void* BindToStorage;
|
||||||
|
public void* Reduce;
|
||||||
|
public void* ComposeWith;
|
||||||
|
public void* Enum;
|
||||||
|
public void* IsEqual;
|
||||||
|
public void* Hash;
|
||||||
|
public void* IsRunning;
|
||||||
|
public void* GetTimeOfLastChange;
|
||||||
|
public void* Inverse;
|
||||||
|
public void* CommonPrefixWith;
|
||||||
|
public void* RelativePathTo;
|
||||||
|
public function [CallingConvention(.Stdcall)] HResult(IMoniker* self, IBindCtx* bindCtx, IMoniker* monikerToLeft, out char16* displayName) GetDisplayName;
|
||||||
|
// ParseDisplayName and IsSystemMoniker follow but are never called.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[CRepr]
|
||||||
|
public struct IEnumMoniker : IUnknown
|
||||||
|
{
|
||||||
|
public const new Guid IID = .(0x00000102, 0x0000, 0x0000, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46);
|
||||||
|
|
||||||
|
public new VTable* VT { get => (.)mVT; }
|
||||||
|
|
||||||
|
/// Returns S_OK if an element was fetched, S_FALSE if the enumeration is exhausted.
|
||||||
|
public HResult Next(uint32 count, out IMoniker* monikers, out uint32 fetchedCount) mut =>
|
||||||
|
VT.Next(&this, count, out monikers, out fetchedCount);
|
||||||
|
|
||||||
|
[CRepr]
|
||||||
|
public struct VTable : IUnknown.VTable
|
||||||
|
{
|
||||||
|
public function [CallingConvention(.Stdcall)] HResult(IEnumMoniker* self, uint32 count, out IMoniker* monikers, out uint32 fetchedCount) Next;
|
||||||
|
public void* Skip;
|
||||||
|
public void* Reset;
|
||||||
|
public void* Clone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[CRepr]
|
||||||
|
public struct IRunningObjectTable : IUnknown
|
||||||
|
{
|
||||||
|
public const new Guid IID = .(0x00000010, 0x0000, 0x0000, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46);
|
||||||
|
|
||||||
|
public new VTable* VT { get => (.)mVT; }
|
||||||
|
|
||||||
|
public HResult GetObject(IMoniker* objectName, out IUnknown* outObject) mut =>
|
||||||
|
VT.GetObject(&this, objectName, out outObject);
|
||||||
|
public HResult EnumRunning(out IEnumMoniker* enumMoniker) mut =>
|
||||||
|
VT.EnumRunning(&this, out enumMoniker);
|
||||||
|
|
||||||
|
[CRepr]
|
||||||
|
public struct VTable : IUnknown.VTable
|
||||||
|
{
|
||||||
|
public void* Register;
|
||||||
|
public void* Revoke;
|
||||||
|
public void* IsRunning;
|
||||||
|
public function [CallingConvention(.Stdcall)] HResult(IRunningObjectTable* self, IMoniker* objectName, out IUnknown* outObject) GetObject;
|
||||||
|
public void* NoteChangeTime;
|
||||||
|
public void* GetTimeOfLastChange;
|
||||||
|
public function [CallingConvention(.Stdcall)] HResult(IRunningObjectTable* self, out IEnumMoniker* enumMoniker) EnumRunning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
Vendored
+1
-1
Submodule GlitchyEngine/vendor/directx updated: 65f5bbbf01...6de8938190
Reference in New Issue
Block a user