Skip to content

Commit ff03b9c

Browse files
Merge pull request #34245 from jasonmalinowski/hack-around-filewatcher-deadlock
Workaround a deadlock caused by watching .editorconfigs
2 parents c92364b + 8097ff9 commit ff03b9c

File tree

2 files changed

+131
-1
lines changed

2 files changed

+131
-1
lines changed

src/EditorFeatures/Core.Wpf/Options/EditorConfigDocumentOptionsProviderFactory.cs

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,162 @@
22

33
using System;
44
using System.Composition;
5+
using System.IO;
6+
using System.Threading.Tasks;
57
using Microsoft.CodeAnalysis.Host.Mef;
68
using Microsoft.CodeAnalysis.Options;
79
using Microsoft.CodeAnalysis.Shared.TestHooks;
810
using Microsoft.VisualStudio.CodingConventions;
11+
using Roslyn.Utilities;
912

1013
namespace Microsoft.CodeAnalysis.Editor.Options
1114
{
1215
[Export(typeof(IDocumentOptionsProviderFactory)), Shared]
1316
class EditorConfigDocumentOptionsProviderFactory : IDocumentOptionsProviderFactory
1417
{
1518
private readonly ICodingConventionsManager _codingConventionsManager;
19+
private readonly IFileWatcher _fileWatcher;
1620
private readonly IAsynchronousOperationListenerProvider _asynchronousOperationListenerProvider;
1721

1822
[ImportingConstructor]
1923
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
2024
public EditorConfigDocumentOptionsProviderFactory(
2125
ICodingConventionsManager codingConventionsManager,
26+
IFileWatcher fileWatcher,
2227
IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider)
2328
{
2429
_codingConventionsManager = codingConventionsManager;
30+
_fileWatcher = fileWatcher;
2531
_asynchronousOperationListenerProvider = asynchronousOperationListenerProvider;
2632
}
2733

2834
public IDocumentOptionsProvider Create(Workspace workspace)
2935
{
30-
return new EditorConfigDocumentOptionsProvider(workspace, _codingConventionsManager, _asynchronousOperationListenerProvider);
36+
ICodingConventionsManager codingConventionsManager;
37+
38+
if (workspace.Kind == WorkspaceKind.RemoteWorkspace)
39+
{
40+
// If it's the remote workspace, it's our own implementation of the file watcher which is already doesn't have
41+
// UI thread dependencies.
42+
codingConventionsManager = _codingConventionsManager;
43+
}
44+
else
45+
{
46+
// The default file watcher implementation inside Visual Studio accientally depends on the UI thread
47+
// (sometimes!) when trying to add a watch to a file. This can cause us to deadlock, since our assumption is
48+
// consumption of a coding convention can be done freely without having to use a JTF-friendly wait.
49+
// So we'll wrap the standard file watcher with one that defers the file watches until later.
50+
var deferredFileWatcher = new DeferredFileWatcher(_fileWatcher, _asynchronousOperationListenerProvider);
51+
codingConventionsManager = CodingConventionsManagerFactory.CreateCodingConventionsManager(deferredFileWatcher);
52+
}
53+
54+
return new EditorConfigDocumentOptionsProvider(workspace, codingConventionsManager, _asynchronousOperationListenerProvider);
55+
}
56+
57+
/// <summary>
58+
/// An implementation of <see cref="IFileWatcher"/> that ensures we don't watch for a file synchronously to
59+
/// avoid deadlocks.
60+
/// </summary>
61+
internal class DeferredFileWatcher : IFileWatcher
62+
{
63+
private readonly IFileWatcher _fileWatcher;
64+
private readonly SimpleTaskQueue _taskQueue = new SimpleTaskQueue(TaskScheduler.Default);
65+
private readonly IAsynchronousOperationListener _listener;
66+
67+
public DeferredFileWatcher(IFileWatcher fileWatcher, IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider)
68+
{
69+
_fileWatcher = fileWatcher;
70+
_fileWatcher.ConventionFileChanged += OnConventionFileChanged;
71+
72+
_listener = asynchronousOperationListenerProvider.GetListener(FeatureAttribute.Workspace);
73+
}
74+
75+
private Task OnConventionFileChanged(object sender, ConventionsFileChangeEventArgs arg)
76+
{
77+
return ConventionFileChanged?.Invoke(this, arg) ?? Task.CompletedTask;
78+
}
79+
80+
public event ConventionsFileChangedAsyncEventHandler ConventionFileChanged;
81+
82+
public event ContextFileMovedAsyncEventHandler ContextFileMoved
83+
{
84+
add
85+
{
86+
_fileWatcher.ContextFileMoved += value;
87+
}
88+
89+
remove
90+
{
91+
_fileWatcher.ContextFileMoved -= value;
92+
}
93+
}
94+
95+
public void Dispose()
96+
{
97+
_fileWatcher.ConventionFileChanged -= OnConventionFileChanged;
98+
_fileWatcher.Dispose();
99+
}
100+
101+
public void StartWatching(string fileName, string directoryPath)
102+
{
103+
var asyncToken = _listener.BeginAsyncOperation(nameof(DeferredFileWatcher) + "." + nameof(StartWatching));
104+
105+
// Read the file time stamp right now; we want to know if it changes between now
106+
// and our ability to get the file watcher in place.
107+
var originalFileTimeStamp = TryGetFileTimeStamp(fileName, directoryPath);
108+
_taskQueue.ScheduleTask(() =>
109+
{
110+
_fileWatcher.StartWatching(fileName, directoryPath);
111+
112+
var newFileTimeStamp = TryGetFileTimeStamp(fileName, directoryPath);
113+
114+
if (originalFileTimeStamp != newFileTimeStamp)
115+
{
116+
ChangeType changeType;
117+
118+
if (!originalFileTimeStamp.HasValue && newFileTimeStamp.HasValue)
119+
{
120+
changeType = ChangeType.FileCreated;
121+
}
122+
else if (originalFileTimeStamp.HasValue && !newFileTimeStamp.HasValue)
123+
{
124+
changeType = ChangeType.FileDeleted;
125+
}
126+
else
127+
{
128+
changeType = ChangeType.FileModified;
129+
}
130+
131+
ConventionFileChanged?.Invoke(this,
132+
new ConventionsFileChangeEventArgs(fileName, directoryPath, changeType));
133+
}
134+
}).CompletesAsyncOperation(asyncToken);
135+
}
136+
137+
private static DateTime? TryGetFileTimeStamp(string fileName, string directoryPath)
138+
{
139+
try
140+
{
141+
var fullFilePath = Path.Combine(directoryPath, fileName);
142+
143+
// Avoid a first-chance exception if the file definitely doesn't exist
144+
if (!File.Exists(fullFilePath))
145+
{
146+
return null;
147+
}
148+
149+
return FileUtilities.GetFileTimeStamp(fullFilePath);
150+
}
151+
catch (IOException)
152+
{
153+
return null;
154+
}
155+
}
156+
157+
public void StopWatching(string fileName, string directoryPath)
158+
{
159+
_taskQueue.ScheduleTask(() => _fileWatcher.StopWatching(fileName, directoryPath));
160+
}
31161
}
32162
}
33163
}

0 commit comments

Comments
 (0)