Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't request DTE via COM since it can hang VS #50

Merged
merged 11 commits into from
Aug 30, 2022
19 changes: 17 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,13 @@ jobs:
submodules: recursive
fetch-depth: 0

- name: ⚙ msbuild
- name: ≥ msbuild
id: msbuild
uses: microsoft/setup-msbuild@v1.1

- name: ≥ defaults
run: echo $((gi '${{ steps.msbuild.outputs.msbuildPath }}\..\..\..\Common7\IDE').FullName.TrimEnd('\')) >> $env:GITHUB_PATH

- name: ≥ version
if: github.event_name == 'release'
shell: bash
Expand All @@ -49,6 +53,15 @@ jobs:
if: always()
run: dotnet pack --no-build -m:1

- name: 🛠 vs
run: |
# Forces a reset, full MEF refresh and exits
devenv .\Xunit.Vsix.sln /NoSplash /ResetSettings General /Command "File.Exit"
wait-process -name 'devenv'
# Same, for the experimental instance
devenv .\Xunit.Vsix.sln /RootSuffix Exp /NoSplash /ResetSettings General /Command "File.Exit"
wait-process -name 'devenv'

- name: 🧪 test
timeout-minutes: 10
uses: ./.github/workflows/test
Expand All @@ -58,7 +71,9 @@ jobs:
if: failure()
with:
name: dumps-${{ github.run_number }}
path: ./**/*.dmp
path: |
./**/*.dmp
./**/*.log

- name: 🚀 sleet
env:
Expand Down
2 changes: 2 additions & 0 deletions src/Xunit.Vsix/IVsRemoteRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ namespace Xunit
{
interface IVsRemoteRunner : IDisposable
{
void EnsureInitialized();

string[][] GetEnvironment();

void Ping();
Expand Down
23 changes: 19 additions & 4 deletions src/Xunit.Vsix/RunningObjects.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public static Interop.DTE GetDTE(TimeSpan retryTimeout)
}
}

public static bool FindDTE(Version visualStudioVersion, int processId)
=> FindMoniker($"!VisualStudio.DTE.{visualStudioVersion.Major}.0:{processId}") != null;

public static Interop.DTE GetDTE(string visualStudioVersion, int processId, TimeSpan retryTimeout)
{
var version = Version.Parse(visualStudioVersion);
Expand Down Expand Up @@ -73,7 +76,19 @@ public static T GetComObject<T>(string monikerName, TimeSpan retryTimeout)

static object GetComObject(string monikerName)
{
object comObject = null;
var moniker = FindMoniker(monikerName);
if (moniker == null)
return null;

if (ErrorHandler.Succeeded(NativeMethods.GetRunningObjectTable(0, out var rdt)) &&
ErrorHandler.Succeeded(rdt.GetObject(moniker, out var comObject)))
return comObject;

return null;
}

static IMoniker? FindMoniker(string monikerName)
{
try
{
IRunningObjectTable table;
Expand All @@ -95,8 +110,7 @@ static object GetComObject(string monikerName)
rgelt[0].GetDisplayName(ctx, null, out displayName);
if (displayName == monikerName)
{
table.GetObject(rgelt[0], out comObject);
return comObject;
return rgelt[0];
}
}
}
Expand All @@ -106,7 +120,8 @@ static object GetComObject(string monikerName)
return null;
}

return comObject;
return null;
}

}
}
27 changes: 21 additions & 6 deletions src/Xunit.Vsix/VsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ bool EnsureConnected(VsixTestCase testCase, IMessageBus messageBus)
Constants.Tracer.TraceEvent(TraceEventType.Verbose, 0, Strings.VsClient.RemoteEnvVars(string.Join(Environment.NewLine,
remoteVars.Select(x => " " + x[0] + "=" + x[1]))));

_runner.EnsureInitialized();

return true;
}

Expand Down Expand Up @@ -303,10 +305,23 @@ bool Start()

Process = Process.Start(info);

// This forces us to wait until VS is fully started.
var dte = RunningObjects.GetDTE(_visualStudioVersion, Process.Id, TimeSpan.FromSeconds(_settings.StartupTimeout));
if (dte == null)
// Wait a bit for warmup, before injecting into the .NET runtime in the VS process.
Thread.Sleep(_settings.WarmupSeconds * 1000);

var start = Stopwatch.StartNew();
var timeout = _settings.StartupTimeoutSeconds * 1000;

var version = new Version(_visualStudioVersion);
var dteFound = false;
// Wait for DTE, but don't retrieve it, since that can cause a hang.
while (start.ElapsedMilliseconds < timeout && !(dteFound = RunningObjects.FindDTE(version, Process.Id)))
Thread.Sleep(100);

if (!dteFound)
{
s_tracer.TraceEvent(TraceEventType.Error, 0, Strings.VsClient.FailedToStart(_visualStudioVersion, _rootSuffix));
return false;
}

try
{
Expand Down Expand Up @@ -338,19 +353,19 @@ bool Start()
});

// Wait max 10 seconds for the injection to succeed
if (!injector.WaitForExit(10000))
if (!injector.WaitForExit(_settings.StartupTimeoutSeconds * 1000))
{
s_tracer.TraceEvent(TraceEventType.Error, 0, Strings.VsClient.FailedToInject(Process.Id));
return false;
}

return injector.ExitCode == 0;
}
catch (Exception ex)
{
s_tracer.TraceEvent(TraceEventType.Error, 0, Strings.VsClient.FailedToInject(Process.Id) + Environment.NewLine + ex.ToString());
return false;
}

return true;
}

void PropagateProfilingVariables(ProcessStartInfo info)
Expand Down
97 changes: 35 additions & 62 deletions src/Xunit.Vsix/VsRemoteRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class VsRemoteRunner : MarshalByRefObject, IVsRemoteRunner
{
string _pipeName;
IChannel _channel;
JoinableTaskContext _jtc;
JoinableTaskFactory _jtf;

Dictionary<Type, object> _assemblyFixtureMappings = new Dictionary<Type, object>();
Dictionary<Type, object> _collectionFixtureMappings = new Dictionary<Type, object>();
Expand All @@ -41,6 +41,37 @@ public VsRemoteRunner()
RemotingServices.Marshal(this, RemotingUtil.HostName);
}

public void EnsureInitialized()
{
if (_jtf != null)
return;

var ev = new ManualResetEventSlim();

var task = ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
{
var shell = await ServiceProvider.GetGlobalServiceAsync<SVsShell, IVsShell>();
while (true)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
shell.GetProperty((int)__VSSPROPID4.VSSPROPID_ShellInitialized, out var value);
if (value is bool initialized && initialized)
break;

await Task.Delay(200);
}

// Ensure MEF too
await ServiceProvider.GetGlobalServiceAsync<SComponentModel, IComponentModel>();

ev.Set();
});

ev.Wait();

_jtf = ThreadHelper.JoinableTaskFactory;
}

public string[][] GetEnvironment()
{
return Environment
Expand All @@ -58,72 +89,17 @@ public string[][] GetEnvironment()

public VsixRunSummary Run(VsixTestCase testCase, IMessageBus messageBus)
{
// Before the first test is run, ensure VS is properly initialized.
if (_jtc == null)
{
var ev = new ManualResetEventSlim();

UIContext.FromUIContextGuid(new(UIContextGuids.NoSolution)).WhenActivated(() =>
{
Task.Run(async () =>
{
var shell = await ServiceProvider.GetGlobalServiceAsync<SVsShell, IVsShell>();
while (true)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
shell.GetProperty((int)__VSSPROPID4.VSSPROPID_ShellInitialized, out var value);
if (value is bool initialized && initialized)
break;

await Task.Delay(200);
}

// Retrieve the component model service, which could also now take time depending on new
// extensions being installed or updated before the first launch.
var components = await ServiceProvider.GetGlobalServiceAsync<SComponentModel, IComponentModel>();
_jtc = components.GetService<JoinableTaskContext>();
ev.Set();

}).Forget();
});

ev.Wait(testCase.TimeoutSeconds * 1000);
ev.Reset();

_jtc.Factory.RunAsync(async () =>
{
await _jtc.Factory.SwitchToMainThreadAsync();

var shell = await ServiceProvider.GetGlobalServiceAsync<SVsShell, IVsShell>();

object zombie;
// __VSSPROPID.VSSPROPID_Zombie
while ((int?)(zombie = shell.GetProperty(-9014, out zombie)) != 0)
{
Thread.Sleep(100);
}

}).Task.ContinueWith(_ => ev.Set(), TaskScheduler.Default).Forget();

ev.Wait(testCase.TimeoutSeconds * 1000);
}

messageBus.QueueMessage(new DiagnosticMessage("Running {0}", testCase.DisplayName));

var aggregator = new ExceptionAggregator();
var runner = _collectionRunnerMap.GetOrAdd(testCase.TestMethod.TestClass.TestCollection,
tc => new VsRemoteTestCollectionRunner(tc, _jtc.Factory, _assemblyFixtureMappings, _collectionFixtureMappings));

if (SynchronizationContext.Current == null)
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
tc => new VsRemoteTestCollectionRunner(tc, _jtf, _assemblyFixtureMappings, _collectionFixtureMappings));

try
{
using (var bus = new TestMessageBus(messageBus))
{
var ev = new ManualResetEventSlim();

var t = _jtc.Factory.RunAsync(async () =>
var t = _jtf.RunAsync(async () =>
(await runner.RunAsync(testCase, bus, aggregator)).ToVsixRunSummary());

_ = t.Task.ContinueWith(_ => ev.Set(), TaskScheduler.Default);
Expand Down Expand Up @@ -175,10 +151,7 @@ public void Start()
_channel = RemotingUtil.CreateChannel(Constants.ServerChannelName, _pipeName);
}

public override object InitializeLifetimeService()
{
return null;
}
public override object InitializeLifetimeService() => null;

class VsRemoteTestCollectionRunner : XunitTestCollectionRunner
{
Expand Down
6 changes: 6 additions & 0 deletions src/Xunit.Vsix/VsixRunnerAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ public class VsixRunnerAttribute : Attribute
/// </summary>
public int StartupTimeout { get; set; }

/// <summary>
/// Wait time in milliseconds to wait for Visual Studio to warm up
/// before injecting into the .NET runtime in it.
/// </summary>
public int WarmupMilliseconds { get; set; }

/// <summary>
/// Specifies the tracing level for runner diagnostics.
/// </summary>
Expand Down
24 changes: 16 additions & 8 deletions src/Xunit.Vsix/VsixRunnerSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ class VsixRunnerSettings
{
public VsixRunnerSettings(int? debuggerAttachRetries = null, int? remoteConnectionRetries = null,
int? processStartRetries = null, int? retrySleepInterval = null,
int? startupTimeout = null)
int? startupTimeoutSeconds = null,
int? warnupSeconds = null)
{
DebuggerAttachRetries = debuggerAttachRetries ?? 5;
RemoteConnectionRetries = remoteConnectionRetries ?? 2;
ProcessStartRetries = processStartRetries ?? 1;
RetrySleepInterval = retrySleepInterval ?? 200;
StartupTimeout = startupTimeout ?? 300;
ProcessStartRetries = processStartRetries ?? 3;
RetrySleepInterval = retrySleepInterval ?? 500;
StartupTimeoutSeconds = startupTimeoutSeconds ?? 30;
WarmupSeconds = warnupSeconds ?? 10;
}

/// <summary>
Expand All @@ -25,7 +27,7 @@ class VsixRunnerSettings
public int RemoteConnectionRetries { get; private set; }

/// <summary>
/// Number of retries to start VS and connect to its DTE service.
/// Number of retries to start VS and ensure proper initialization.
/// </summary>
public int ProcessStartRetries { get; private set; }

Expand All @@ -37,9 +39,15 @@ class VsixRunnerSettings
public int RetrySleepInterval { get; private set; }

/// <summary>
/// The timeout in seconds to wait for Visual Studio to
/// start and initialize its DTE automation model.
/// The timeout in seconds to for the test runner to successfully
/// inject itself into the Visual Studio .NET runtime.
/// </summary>
public int StartupTimeout { get; private set; }
public int StartupTimeoutSeconds { get; private set; }

/// <summary>
/// Wait time in seconds for Visual Studio to warm up
/// before injecting into the .NET runtime in it.
/// </summary>
public int WarmupSeconds { get; private set; }
}
}
3 changes: 2 additions & 1 deletion src/Xunit.Vsix/VsixTestCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ class VsixTestCollection : TestCollection
settingsAttribute.GetInitializedArgument<int?>(nameof(VsixRunnerSettings.RemoteConnectionRetries)),
settingsAttribute.GetInitializedArgument<int?>(nameof(VsixRunnerSettings.ProcessStartRetries)),
settingsAttribute.GetInitializedArgument<int?>(nameof(VsixRunnerSettings.RetrySleepInterval)),
settingsAttribute.GetInitializedArgument<int?>(nameof(VsixRunnerSettings.StartupTimeout)));
settingsAttribute.GetInitializedArgument<int?>(nameof(VsixRunnerSettings.StartupTimeoutSeconds)),
settingsAttribute.GetInitializedArgument<int?>(nameof(VsixRunnerSettings.WarmupSeconds)));
}

public string VisualStudioVersion { get; }
Expand Down
4 changes: 1 addition & 3 deletions src/Xunit.Vsix/build/xunit.runner.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
{
"shadowCopy": false,
"diagnosticMessages": true,
"internalDiagnosticMessages": true
"shadowCopy": false
}