Skip to content

Commit 2a47a4d

Browse files
authored
Merge pull request #4426 from bclothier/ComSafeSerialize
Introduce Serialize method for COM Safe
2 parents 126d05d + e62dcef commit 2a47a4d

File tree

5 files changed

+371
-28
lines changed

5 files changed

+371
-28
lines changed

Rubberduck.Core/UI/Command/MenuItems/CommandBars/SerializeProjectsCommandMenuItem.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ protected override void OnExecute(object parameter)
6666
if (!toSerialize.ContainsKey(type.Guid))
6767
{
6868
toSerialize.Add(type.Guid, type);
69-
}
69+
}
7070
}
7171
}
7272
}
@@ -76,6 +76,20 @@ protected override void OnExecute(object parameter)
7676
Logger.Warn($"Serializing {library.Path}.");
7777
_serializationProvider.SerializeProject(library);
7878
}
79+
80+
#if DEBUG
81+
//This block must be inside a DEBUG block because the Serialize method
82+
//called is conditionally compiled and available only for a DEBUG build.
83+
var path = !string.IsNullOrWhiteSpace(_serializationProvider.Target)
84+
? Path.GetDirectoryName(_serializationProvider.Target)
85+
: Path.GetTempPath();
86+
var traceDirectory = Path.Combine(path, "COM Trace");
87+
if (!Directory.Exists(traceDirectory))
88+
{
89+
Directory.CreateDirectory(traceDirectory);
90+
}
91+
Rubberduck.VBEditor.ComManagement.ComSafeManager.GetCurrentComSafe().Serialize(traceDirectory);
92+
#endif
7993
}
8094
}
8195
}
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Runtime.CompilerServices;
4+
using Rubberduck.VBEditor.SafeComWrappers.Abstract;
5+
6+
#if DEBUG
7+
using System.Diagnostics;
8+
using System.IO;
9+
using System.Runtime.InteropServices;
10+
#endif
11+
12+
namespace Rubberduck.VBEditor.ComManagement
13+
{
14+
public abstract class ComSafeBase : IComSafe
15+
{
16+
public abstract void Add(ISafeComWrapper comWrapper);
17+
18+
public abstract bool TryRemove(ISafeComWrapper comWrapper);
19+
20+
//We do not use GetHashCode because subclasses of SafeComWrapper<T> overwrite this method
21+
//and we need to distinguish between individual instances.
22+
protected int GetComWrapperObjectHashCode(ISafeComWrapper comWrapper)
23+
{
24+
return RuntimeHelpers.GetHashCode(comWrapper);
25+
}
26+
27+
private bool _disposed;
28+
public void Dispose()
29+
{
30+
Dispose(true);
31+
32+
#if DEBUG
33+
if (_disposed)
34+
{
35+
return;
36+
}
37+
38+
_disposed = true;
39+
40+
lock (_streamLock)
41+
{
42+
try
43+
{
44+
if (_traceStream == null)
45+
{
46+
return;
47+
}
48+
49+
_traceStream.Close();
50+
if (string.IsNullOrWhiteSpace(_directory))
51+
{
52+
File.Delete(_traceFilePath);
53+
}
54+
else
55+
{
56+
File.Move(_traceFilePath,
57+
Path.Combine(_directory,
58+
Path.GetFileNameWithoutExtension(_traceFilePath) + " final.csv"));
59+
}
60+
}
61+
finally
62+
{
63+
_traceStream?.Dispose();
64+
_traceStream = null;
65+
}
66+
}
67+
#endif
68+
}
69+
70+
protected abstract void Dispose(bool disposing);
71+
72+
#if DEBUG
73+
private struct TraceData
74+
{
75+
internal int HashCode { get; set; }
76+
internal string IUnknownAddress { get; set; }
77+
internal IEnumerable<string> StackTrace { get; set; }
78+
}
79+
private StreamWriter _traceStream;
80+
private string _traceFilePath;
81+
private string _directory;
82+
private readonly object _streamLock = new object();
83+
84+
/// <summary>
85+
/// The first few stack frames come from the ComSafe and thus are not
86+
/// particularly interesting. Typically, we want to look at the frames
87+
/// outside the ComSafe.
88+
/// </summary>
89+
private const int StackTraceNumberOfElementsToSkipOnRemoval = 6;
90+
private const int StackTrackNumberOfElementsToSkipOnAddUpdate = 8;
91+
private const int StackTraceDepth = 5;
92+
93+
/// <inheritdoc cref="IComSafe.Serialize"/>
94+
public void Serialize(string targetDirectory)
95+
{
96+
lock (_streamLock)
97+
{
98+
_directory = targetDirectory;
99+
var serializeTime = DateTime.UtcNow;
100+
using (var stream = File.AppendText(Path.Combine(_directory,
101+
$"COM Safe Content Snapshot {serializeTime:yyyyMMddhhmmss}.csv")))
102+
{
103+
stream.WriteLine(
104+
$"Ordinal\tKey\tCOM Wrapper Type\tWrapping Null?\tIUnknown Pointer Address");
105+
var i = 0;
106+
foreach (var kvp in GetWrappers())
107+
{
108+
var line = kvp.Value != null
109+
? $"{i++}\t{kvp.Key}\t\"{kvp.Value.GetType().FullName}\"\t\"{kvp.Value.IsWrappingNullReference}\"\t\"{(kvp.Value.IsWrappingNullReference ? "null" : GetPtrAddress(kvp.Value.Target))}\""
110+
: $"{i++}\t{kvp.Key}\t\"null\"\t\"null\"\t\"null\"";
111+
stream.WriteLine(line);
112+
}
113+
}
114+
115+
if (_traceStream == null)
116+
{
117+
return;
118+
}
119+
120+
_traceStream.Flush();
121+
File.Copy(_traceFilePath, Path.Combine(_directory, $"COM Safe Stack Trace {serializeTime:yyyyMMddhhmmss}.csv"));
122+
}
123+
}
124+
125+
protected void TraceAdd(ISafeComWrapper comWrapper)
126+
{
127+
Trace("Add", comWrapper, StackTrackNumberOfElementsToSkipOnAddUpdate);
128+
}
129+
130+
protected void TraceUpdate(ISafeComWrapper comWrapper)
131+
{
132+
Trace("Update", comWrapper, StackTrackNumberOfElementsToSkipOnAddUpdate);
133+
}
134+
135+
protected void TraceRemove(ISafeComWrapper comWrapper, bool wasRemoved)
136+
{
137+
var activity = wasRemoved ? "Removed" : "Not removed";
138+
Trace(activity, comWrapper, StackTraceNumberOfElementsToSkipOnRemoval);
139+
}
140+
141+
private readonly object _idLock = new object();
142+
private int _id;
143+
private void Trace(string activity, ISafeComWrapper comWrapper, int framesToSkip)
144+
{
145+
lock (_streamLock)
146+
{
147+
if (_disposed)
148+
{
149+
return;
150+
}
151+
152+
if (_traceStream == null)
153+
{
154+
var directory = Path.GetTempPath();
155+
_traceFilePath = Path.Combine(directory,
156+
$"COM Safe Stack Trace {DateTime.UtcNow:yyyyMMddhhmmss}.{GetHashCode()}.csv");
157+
_traceStream = File.AppendText(_traceFilePath);
158+
_traceStream.WriteLine(
159+
$"Ordinal\tTimestamp\tActivity\tKey\tIUnknown Pointer Address\t{FrameHeaders()}");
160+
}
161+
162+
int id;
163+
lock (_idLock)
164+
{
165+
id = _id++;
166+
}
167+
168+
var traceData = new TraceData
169+
{
170+
HashCode = GetComWrapperObjectHashCode(comWrapper),
171+
IUnknownAddress = comWrapper.IsWrappingNullReference ? "null" : GetPtrAddress(comWrapper.Target),
172+
StackTrace = GetStackTrace(StackTraceDepth, framesToSkip)
173+
};
174+
175+
var line =
176+
$"{id}\t{DateTime.UtcNow}\t\"{activity}\"\t{traceData.HashCode}\t{traceData.IUnknownAddress}\t\"{string.Join("\"\t\"", traceData.StackTrace)}\"";
177+
_traceStream.WriteLine(line);
178+
}
179+
}
180+
181+
private static string FrameHeaders()
182+
{
183+
var headers = new System.Text.StringBuilder();
184+
for(var i = 1; i <= StackTraceDepth; i++)
185+
{
186+
headers.Append($"Frame {i}\t");
187+
}
188+
189+
return headers.ToString();
190+
}
191+
192+
protected abstract IDictionary<int, ISafeComWrapper> GetWrappers();
193+
194+
private static IEnumerable<string> GetStackTrace(int frames, int framesToSkip)
195+
{
196+
var list = new List<string>();
197+
var trace = new StackTrace();
198+
if (trace.FrameCount < (frames + framesToSkip))
199+
{
200+
frames = trace.FrameCount;
201+
}
202+
else
203+
{
204+
frames += framesToSkip;
205+
}
206+
207+
framesToSkip -= 1;
208+
frames -= 1;
209+
210+
for (var i = framesToSkip; i < frames; i++)
211+
{
212+
var frame = trace.GetFrame(i);
213+
var type = frame.GetMethod().DeclaringType;
214+
215+
var typeName = type?.FullName ?? string.Empty;
216+
var methodName = frame.GetMethod().Name;
217+
218+
var qualifiedName = $"{typeName}{(typeName.Length > 0 ? "::" : string.Empty)}{methodName}";
219+
list.Add(qualifiedName);
220+
}
221+
222+
return list;
223+
}
224+
225+
protected static string GetPtrAddress(object target)
226+
{
227+
if (target == null)
228+
{
229+
return IntPtr.Zero.ToString();
230+
}
231+
232+
if (!Marshal.IsComObject(target))
233+
{
234+
return "Not a COM object";
235+
}
236+
237+
var pointer = IntPtr.Zero;
238+
try
239+
{
240+
pointer = Marshal.GetIUnknownForObject(target);
241+
}
242+
finally
243+
{
244+
if (pointer != IntPtr.Zero)
245+
{
246+
Marshal.Release(pointer);
247+
}
248+
}
249+
250+
return pointer.ToString();
251+
}
252+
#endif
253+
}
254+
}

Rubberduck.VBEEditor/ComManagement/IComSafe.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,14 @@ public interface IComSafe: IDisposable
77
{
88
void Add(ISafeComWrapper comWrapper);
99
bool TryRemove(ISafeComWrapper comWrapper);
10+
#if DEBUG
11+
/// <summary>
12+
/// Available in DEBUG build only. Provide a mechanism for serializing both
13+
/// a snapshot of the COM safe at the instant and a historical activity log
14+
/// with a limited stack trace for each entry.
15+
/// </summary>
16+
/// <param name="targetDirectory">The path to a directory to place the serialized files in</param>
17+
void Serialize(string targetDirectory);
18+
#endif
1019
}
1120
}
Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,55 @@
1-
using System;
2-
using System.Collections.Concurrent;
1+
using System.Collections.Concurrent;
2+
using System.Collections.Generic;
3+
using System.Linq;
34
using Rubberduck.VBEditor.SafeComWrappers.Abstract;
45

56
namespace Rubberduck.VBEditor.ComManagement
67
{
7-
public class StrongComSafe: IComSafe
8+
public class StrongComSafe: ComSafeBase
89
{
910
//We override the equality comparison and hash code because subclasses of SafeComWrapper<T> override the corresponding methods.
1011
//We need to distinguish between the individual wrapper instances no matter whether they are semantically equal.
1112
private readonly ConcurrentDictionary<ISafeComWrapper, byte> _comWrapperCache = new ConcurrentDictionary<ISafeComWrapper, byte>(new ReferenceEqualityComparer());
1213

13-
14-
public void Add(ISafeComWrapper comWrapper)
14+
public override void Add(ISafeComWrapper comWrapper)
1515
{
1616
if (comWrapper != null)
1717
{
18-
_comWrapperCache.AddOrUpdate(comWrapper, key => 1, (key, value) => value);
18+
_comWrapperCache.AddOrUpdate(
19+
comWrapper,
20+
key =>
21+
{
22+
#if DEBUG
23+
TraceAdd(comWrapper);
24+
#endif
25+
return 1;
26+
},
27+
(key, value) =>
28+
{
29+
#if DEBUG
30+
TraceUpdate(comWrapper);
31+
#endif
32+
return value;
33+
});
1934
}
20-
2135
}
2236

23-
public bool TryRemove(ISafeComWrapper comWrapper)
37+
public override bool TryRemove(ISafeComWrapper comWrapper)
2438
{
25-
return !_disposed && comWrapper != null && _comWrapperCache.TryRemove(comWrapper, out _);
39+
if (_disposed || comWrapper == null)
40+
{
41+
return false;
42+
}
43+
44+
var result = _comWrapperCache.TryRemove(comWrapper, out _);
45+
#if DEBUG
46+
TraceRemove(comWrapper, result);
47+
#endif
48+
return result;
2649
}
2750

2851
private bool _disposed;
29-
public void Dispose()
52+
protected override void Dispose(bool disposing)
3053
{
3154
if (_disposed)
3255
{
@@ -42,5 +65,13 @@ public void Dispose()
4265

4366
_comWrapperCache.Clear();
4467
}
68+
69+
#if DEBUG
70+
protected override IDictionary<int, ISafeComWrapper> GetWrappers()
71+
{
72+
return _comWrapperCache.Keys.ToDictionary(GetComWrapperObjectHashCode);
73+
}
74+
#endif
4575
}
4676
}
77+

0 commit comments

Comments
 (0)