Skip to content

Commit 00c37d3

Browse files
committed
Improve resiliency when invoking DTE
Various minor improvements including: * Not starting the active document monitoring until initial build completes * Retrying opening of the startup file if ExecuteCommand fails (IDE might be busy doing something else on the UI thread) * Not trying to open startup file if we can't determine which one * Some troubleshooting aids by capturing thrown exceptions for Debug.Fail diagnostics Fixes #18
1 parent 7394bee commit 00c37d3

File tree

7 files changed

+101
-49
lines changed

7 files changed

+101
-49
lines changed

src/Directory.props

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
<PackOnBuild>true</PackOnBuild>
77
<PackageProjectUrl>https://clarius.org/SmallSharp</PackageProjectUrl>
88
<NoWarn>NU1702;$(NoWarn)</NoWarn>
9+
10+
<StartAction>Program</StartAction>
11+
<StartProgram>$(VsInstallRoot)\Common7\IDE\devenv.exe</StartProgram>
912
</PropertyGroup>
1013

1114
</Project>

src/SmallSharp.Build/ActiveDocumentMonitor.cs

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Diagnostics;
44
using System.IO;
55
using System.Linq;
6+
using System.Threading;
67
using System.Xml.Linq;
78
using Microsoft.VisualStudio.Shell.Interop;
89
using Newtonsoft.Json.Linq;
@@ -12,10 +13,12 @@ namespace SmallSharp.Build
1213
class ActiveDocumentMonitor : MarshalByRefObject, IDisposable, IVsRunningDocTableEvents, IVsSelectionEvents
1314
{
1415
FileSystemWatcher watcher;
15-
readonly IVsRunningDocumentTable? rdt;
16-
readonly IVsMonitorSelection? selection;
17-
readonly uint rdtCookie;
18-
readonly uint selectionCookie;
16+
readonly IServiceProvider services;
17+
18+
IVsRunningDocumentTable? rdt;
19+
IVsMonitorSelection? selection;
20+
uint rdtCookie;
21+
uint selectionCookie;
1922

2023
string launchProfilesPath;
2124
string userFile;
@@ -27,6 +30,7 @@ public ActiveDocumentMonitor(string launchProfilesPath, string userFile, string
2730
this.launchProfilesPath = launchProfilesPath;
2831
this.userFile = userFile;
2932
this.flagFile = flagFile;
33+
this.services = services;
3034

3135
watcher = new FileSystemWatcher(Path.GetDirectoryName(launchProfilesPath))
3236
{
@@ -38,7 +42,10 @@ public ActiveDocumentMonitor(string launchProfilesPath, string userFile, string
3842
watcher.Created += (_, _) => ReloadProfiles();
3943
watcher.EnableRaisingEvents = true;
4044
ReloadProfiles();
45+
}
4146

47+
public void Start()
48+
{
4249
rdt = (IVsRunningDocumentTable)services.GetService(typeof(SVsRunningDocumentTable));
4350
if (rdt != null)
4451
rdt.AdviseRunningDocTableEvents(this, out rdtCookie);
@@ -62,20 +69,32 @@ void ReloadProfiles()
6269
if (!File.Exists(launchProfilesPath))
6370
return;
6471

65-
try
66-
{
67-
var json = JObject.Parse(File.ReadAllText(launchProfilesPath));
68-
if (json.Property("profiles") is not JProperty prop ||
69-
prop.Value is not JObject profiles)
70-
return;
72+
var maxAttempts = 5;
73+
var exceptions = new List<Exception>();
7174

72-
startupFiles = profiles.Properties().Select(p => p.Name)
73-
.ToDictionary(x => x, StringComparer.OrdinalIgnoreCase);
74-
}
75-
catch
75+
for (var i = 0; i < maxAttempts; i++)
7676
{
77-
Debug.Fail("Could not read launchSettings.json");
77+
try
78+
{
79+
var json = JObject.Parse(File.ReadAllText(launchProfilesPath));
80+
if (json.Property("profiles") is not JProperty prop ||
81+
prop.Value is not JObject profiles)
82+
return;
83+
84+
startupFiles = profiles.Properties().Select(p => p.Name)
85+
.ToDictionary(x => x, StringComparer.OrdinalIgnoreCase);
86+
87+
return;
88+
}
89+
catch (Exception e)
90+
{
91+
exceptions.Add(e);
92+
Thread.Sleep(500);
93+
}
7894
}
95+
96+
// NOTE: check exceptions list to see why.
97+
Debug.Fail("Could not read launchSettings.json");
7998
}
8099

81100
void UpdateStartupFile(string? path)
@@ -105,24 +124,30 @@ void UpdateStartupFile(string? path)
105124
xdoc.Save(userFile);
106125
}
107126
}
108-
catch
127+
catch (Exception e)
109128
{
110-
Debug.Fail("Failed to load or update .user file.");
129+
Debug.Fail($"Failed to load or update .user file: {e}");
111130
}
112131
}
113132
}
114133

115134
void IDisposable.Dispose()
116135
{
117136
if (rdtCookie != 0 && rdt != null)
118-
rdt.UnadviseRunningDocTableEvents(rdtCookie);
137+
Try(() => rdt.UnadviseRunningDocTableEvents(rdtCookie));
119138

120139
if (selectionCookie != 0 && selection != null)
121-
selection.UnadviseSelectionEvents(selectionCookie);
140+
Try(() => selection.UnadviseSelectionEvents(selectionCookie));
122141

123142
watcher.Dispose();
124143
}
125144

145+
void Try(Action action)
146+
{
147+
try { action(); }
148+
catch (Exception e) { Debug.WriteLine(e); }
149+
}
150+
126151
int IVsRunningDocTableEvents.OnAfterAttributeChange(uint docCookie, uint grfAttribs)
127152
{
128153
// The MSBuild targets should have created it in target SelectStartupFile.

src/SmallSharp.Build/MonitorActiveDocument.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,19 @@ public override bool Execute()
2727
{
2828
if (WindowsInterop.GetServiceProvider() is IServiceProvider services)
2929
{
30-
BuildEngine4.RegisterTaskObject(nameof(ActiveDocumentMonitor),
31-
new ActiveDocumentMonitor(LaunchProfiles, UserFile, FlagFile, services),
30+
var documentMonitor = new ActiveDocumentMonitor(LaunchProfiles, UserFile, FlagFile, services);
31+
32+
BuildEngine4.RegisterTaskObject(nameof(ActiveDocumentMonitor), documentMonitor,
3233
RegisteredTaskObjectLifetime.AppDomain, false);
34+
35+
// Start monitoring at the end of the build, to avoid slowing down the DTB
36+
BuildEngine4.RegisterTaskObject("StartMonitor",
37+
new DisposableAction(() => documentMonitor.Start()),
38+
RegisteredTaskObjectLifetime.Build, false);
39+
}
40+
else
41+
{
42+
Debug.Fail("Failed to get IServiceProvider to monitor for active document.");
3343
}
3444
}
3545
else

src/SmallSharp.Build/OpenStartupFile.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using System;
2-
using System.IO;
3-
using EnvDTE;
1+
using System.IO;
42
using Microsoft.Build.Framework;
53
using Microsoft.Build.Utilities;
64

@@ -15,7 +13,7 @@ public class OpenStartupFile : Task
1513

1614
public override bool Execute()
1715
{
18-
if (FlagFile == null || StartupFile == null)
16+
if (string.IsNullOrEmpty(FlagFile) || string.IsNullOrEmpty(StartupFile))
1917
return true;
2018

2119
if (!File.Exists(FlagFile) ||
@@ -24,7 +22,7 @@ public override bool Execute()
2422
// This defers the opening until the build completes.
2523
BuildEngine4.RegisterTaskObject(
2624
StartupFile,
27-
new DisposableAction(() => WindowsInterop.EnsureOpened(StartupFile)),
25+
new DisposableAction(() => WindowsInterop.EnsureOpened(StartupFile!)),
2826
RegisteredTaskObjectLifetime.Build, false);
2927

3028
File.WriteAllText(FlagFile, StartupFile);

src/SmallSharp.Build/SmallSharp.Build.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<PropertyGroup>
44
<TargetFramework>net472</TargetFramework>
55
<PackFolder>build\netstandard2.0</PackFolder>
6+
<PackNone>true</PackNone>
67
</PropertyGroup>
78

89
<ItemGroup>
@@ -17,4 +18,8 @@
1718
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" PrivateAssets="all" />
1819
</ItemGroup>
1920

21+
<ItemGroup>
22+
<None Update="SmallSharp.targets" CopyToOutputDirectory="PreserveNewest" />
23+
</ItemGroup>
24+
2025
</Project>

src/SmallSharp/SmallSharp.targets renamed to src/SmallSharp.Build/SmallSharp.targets

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,13 @@
44
<UsingTask AssemblyFile="SmallSharp.Build.dll" TaskName="OpenStartupFile" Condition="'$(BuildingInsideVisualStudio)' == 'true'" />
55

66
<PropertyGroup>
7-
<DefaultItemExcludesInProjectFolder>*$(DefaultLanguageSourceExtension)</DefaultItemExcludesInProjectFolder>
87
<MSBuildShortVersion>$(MSBuildVersion.TrimEnd('0123456789').TrimEnd('.'))</MSBuildShortVersion>
98
<UserProjectNamespace>
109
<Namespace Prefix="msb" Uri="http://schemas.microsoft.com/developer/msbuild/2003" />
1110
</UserProjectNamespace>
1211
<StartupFile>$(ActiveDebugProfile)</StartupFile>
1312
</PropertyGroup>
1413

15-
<ItemGroup>
16-
<None Include="*$(DefaultLanguageSourceExtension)" Exclude="$(ActiveDebugProfile)" />
17-
<Compile Include="$(ActiveDebugProfile)" Condition="Exists('$(ActiveDebugProfile)')" />
18-
</ItemGroup>
19-
2014
<ItemGroup>
2115
<CompilerVisibleProperty Include="MSBuildProjectDirectory" />
2216
<CompilerVisibleProperty Include="ActiveDebugProfile" />
@@ -27,12 +21,20 @@
2721
</ItemGroup>
2822

2923
<!-- NOTE: we only require VS16.8+ when running in the IDE, since for CLI builds we just do targets stuff -->
30-
<Target Name="EnsureVisualStudio" BeforeTargets="BeforeCompile;CoreCompile"
24+
<Target Name="EnsureVisualStudio" BeforeTargets="BeforeCompile;CoreCompile"
3125
Condition="$(MSBuildShortVersion) &lt; '16.8' and '$(BuildingInsideVisualStudio)' == 'true'">
3226
<!-- Top-level programs require this, so does our source generator. -->
3327
<Error Text="SmallSharp requires Visual Studio 16.8 or greater." />
3428
</Target>
3529

30+
<Target Name="SelectTopLevelCompile" BeforeTargets="BeforeCompile;CoreCompile;CompileDesignTime" DependsOnTargets="SelectStartupFile">
31+
<ItemGroup>
32+
<None Include="*$(DefaultLanguageSourceExtension)" />
33+
<Compile Remove="*$(DefaultLanguageSourceExtension)" />
34+
<Compile Include="$(StartupFile)" Condition="Exists('$(StartupFile)')" />
35+
</ItemGroup>
36+
</Target>
37+
3638
<Target Name="SelectStartupFile" BeforeTargets="BeforeCompile;CoreCompile"
3739
Condition="'$(StartupFile)' == '' or !Exists('$(StartupFile)')">
3840

@@ -122,12 +124,9 @@
122124
<Warning Text="Could not set ActiveDebugProfile=$(StartupFile). Run the project once to fix it."
123125
Condition="'$(StartupFile)' != '' and '$(StartupDebugProfile)' != '$(StartupFile)'"/>
124126

125-
<ItemGroup>
126-
<Compile Include="$(StartupFile)" Condition="'$(StartupFile)' != ''" />
127-
</ItemGroup>
128127
</Target>
129128

130-
<Target Name="OpenStartupFile"
129+
<Target Name="OpenStartupFile"
131130
BeforeTargets="CompileDesignTime"
132131
DependsOnTargets="SelectStartupFile"
133132
Condition="'$(OpenStartupFile)' != 'false' and
@@ -144,7 +143,7 @@
144143
<Target Name="MonitorActiveDocument" AfterTargets="CompileDesignTime">
145144
<MonitorActiveDocument FlagFile="$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(BaseIntermediateOutputPath)', 'OpenedStartupFile.txt'))"
146145
LaunchProfiles="$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', 'Properties', 'launchSettings.json'))"
147-
UserFile="$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(MSBuildProjectFile).user'))" />
146+
UserFile="$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(MSBuildProjectFile).user'))" />
148147
</Target>
149148

150149
<!-- Adds the additional files that the source generator uses to emit the launch profiles. -->

src/SmallSharp.Build/WindowsInterop.cs

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Diagnostics;
34
using System.IO;
45
using System.Linq;
@@ -21,18 +22,29 @@ public static void EnsureOpened(string filePath, TimeSpan delay = default)
2122
if (delay != default)
2223
Thread.Sleep(delay);
2324

24-
try
25-
{
26-
var dte = GetDTE();
27-
if (dte == null)
28-
return;
25+
var dte = GetDTE();
26+
if (dte == null)
27+
return;
2928

30-
dte.ExecuteCommand("File.OpenFile", filePath);
31-
}
32-
catch
29+
var maxAttempts = 5;
30+
var exceptions = new List<Exception>();
31+
32+
for (var i = 0; i < maxAttempts; i++)
3333
{
34-
Debug.Fail($"Failed to open {filePath}.");
34+
try
35+
{
36+
dte.ExecuteCommand("File.OpenFile", filePath);
37+
return;
38+
}
39+
catch (Exception e)
40+
{
41+
exceptions.Add(e);
42+
Thread.Sleep(500);
43+
}
3544
}
45+
46+
// NOTE: inspect exceptions variable
47+
Debug.Fail($"Failed to open {filePath} after 5 attempts.");
3648
}
3749

3850
public static IServiceProvider? GetServiceProvider(TimeSpan delay = default)
@@ -52,9 +64,9 @@ public static void EnsureOpened(string filePath, TimeSpan delay = default)
5264

5365
return new OleServiceProvider(dte);
5466
}
55-
catch
67+
catch (Exception e)
5668
{
57-
Debug.Fail("Failed to get IDE service provider.");
69+
Debug.Fail($"Failed to get IDE service provider: {e}");
5870
return null;
5971
}
6072
}

0 commit comments

Comments
 (0)