source: branches/eraser6/BlackBox/Eraser.Util/BlackBox.cs @ 1608

Revision 1608, 22.4 KB checked in by lowjoel, 5 years ago (diff)

Always write the debug log, the screenshot, then the memory dump as that's the usual order for files in increasing size. This helps with situations where the disk is full. Addresses #315: Fix handling of corrupt crash reports

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1/*
2 * $Id$
3 * Copyright 2008-2009 The Eraser Project
4 * Original Author: Joel Low <lowjoel@users.sourceforge.net>
5 * Modified By:
6 *
7 * This file is part of Eraser.
8 *
9 * Eraser is free software: you can redistribute it and/or modify it under the
10 * terms of the GNU General Public License as published by the Free Software
11 * Foundation, either version 3 of the License, or (at your option) any later
12 * version.
13 *
14 * Eraser is distributed in the hope that it will be useful, but WITHOUT ANY
15 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
16 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
17 *
18 * A copy of the GNU General Public License can be found at
19 * <http://www.gnu.org/licenses/>.
20 */
21
22using System;
23using System.Collections.Generic;
24using System.Text;
25using System.Windows.Forms;
26using System.IO;
27using System.Runtime.InteropServices;
28using Microsoft.Win32.SafeHandles;
29using System.Diagnostics;
30using System.Threading;
31using System.Drawing;
32using System.Drawing.Imaging;
33using System.Reflection;
34using System.Collections.ObjectModel;
35using System.Globalization;
36
37namespace Eraser.Util
38{
39    /// <summary>
40    /// Handles application exceptions, stores minidumps and uploads them to the
41    /// Eraser server.
42    /// </summary>
43    public class BlackBox
44    {
45        /// <summary>
46        /// Stores DLL references for this class.
47        /// </summary>
48        private static class NativeMethods
49        {
50            /// <summary>
51            /// Writes user-mode minidump information to the specified file.
52            /// </summary>
53            /// <param name="hProcess">A handle to the process for which the information
54            /// is to be generated.</param>
55            /// <param name="ProcessId">The identifier of the process for which the information
56            /// is to be generated.</param>
57            /// <param name="hFile">A handle to the file in which the information is to be
58            /// written.</param>
59            /// <param name="DumpType">The type of information to be generated. This parameter
60            /// can be one or more of the values from the MINIDUMP_TYPE enumeration.</param>
61            /// <param name="ExceptionParam">A pointer to a MiniDumpExceptionInfo structure
62            /// describing the client exception that caused the minidump to be generated.
63            /// If the value of this parameter is NULL, no exception information is included
64            /// in the minidump file.</param>
65            /// <param name="UserStreamParam">Not supported. Use IntPtr.Zero</param>
66            /// <param name="CallbackParam">Not supported. Use IntPtr.Zero</param>
67            /// <returns>If the function succeeds, the return value is true; otherwise, the
68            /// return value is false. To retrieve extended error information, call GetLastError.
69            /// Note that the last error will be an HRESULT value.</returns>
70            [DllImport("dbghelp.dll", SetLastError = true)]
71            [return: MarshalAs(UnmanagedType.Bool)]
72            public static extern bool MiniDumpWriteDump(IntPtr hProcess, uint ProcessId,
73                SafeFileHandle hFile, MiniDumpType DumpType,
74                ref MiniDumpExceptionInfo ExceptionParam, IntPtr UserStreamParam,
75                IntPtr CallbackParam);
76
77            /// <summary>
78            /// Identifies the type of information that will be written to the minidump file
79            /// by the MiniDumpWriteDump function.
80            /// </summary>
81            public enum MiniDumpType
82            {
83                /// <summary>
84                /// Include just the information necessary to capture stack traces for all
85                /// existing threads in a process.
86                /// </summary>
87                MiniDumpNormal = 0x00000000,
88
89                /// <summary>
90                /// Include the data sections from all loaded modules. This results in the
91                /// inclusion of global variables, which can make the minidump file significantly
92                /// larger. For per-module control, use the ModuleWriteDataSeg enumeration
93                /// value from MODULE_WRITE_FLAGS.
94                /// </summary>
95                MiniDumpWithDataSegs = 0x00000001,
96
97                /// <summary>
98                /// Include all accessible memory in the process. The raw memory data is
99                /// included at the end, so that the initial structures can be mapped directly
100                /// without the raw memory information. This option can result in a very large
101                /// file.
102                /// </summary>
103                MiniDumpWithFullMemory = 0x00000002,
104
105                /// <summary>
106                /// Include high-level information about the operating system handles that are
107                /// active when the minidump is made.
108                /// </summary>
109                MiniDumpWithHandleData = 0x00000004,
110
111                /// <summary>
112                /// Stack and backing store memory written to the minidump file should be
113                /// filtered to remove all but the pointer values necessary to reconstruct a
114                /// stack trace. Typically, this removes any private information.
115                /// </summary>
116                MiniDumpFilterMemory = 0x00000008,
117
118                /// <summary>
119                /// Stack and backing store memory should be scanned for pointer references
120                /// to modules in the module list. If a module is referenced by stack or backing
121                /// store memory, the ModuleWriteFlags member of the MINIDUMP_CALLBACK_OUTPUT
122                /// structure is set to ModuleReferencedByMemory.
123                /// </summary>
124                MiniDumpScanMemory = 0x00000010,
125
126                /// <summary>
127                /// Include information from the list of modules that were recently unloaded,
128                /// if this information is maintained by the operating system.
129                /// </summary>
130                MiniDumpWithUnloadedModules = 0x00000020,
131
132                /// <summary>
133                /// Include pages with data referenced by locals or other stack memory.
134                /// This option can increase the size of the minidump file significantly.
135                /// </summary>
136                MiniDumpWithIndirectlyReferencedMemory = 0x00000040,
137
138                /// <summary>
139                /// Filter module paths for information such as user names or important
140                /// directories. This option may prevent the system from locating the image
141                /// file and should be used only in special situations.
142                /// </summary>
143                MiniDumpFilterModulePaths = 0x00000080,
144
145                /// <summary>
146                /// Include complete per-process and per-thread information from the operating
147                /// system.
148                /// </summary>
149                MiniDumpWithProcessThreadData = 0x00000100,
150
151                /// <summary>
152                /// Scan the virtual address space for PAGE_READWRITE memory to be included.
153                /// </summary>
154                MiniDumpWithPrivateReadWriteMemory = 0x00000200,
155
156                /// <summary>
157                /// Reduce the data that is dumped by eliminating memory regions that are not
158                /// essential to meet criteria specified for the dump. This can avoid dumping
159                /// memory that may contain data that is private to the user. However, it is
160                /// not a guarantee that no private information will be present.
161                /// </summary>
162                MiniDumpWithoutOptionalData = 0x00000400,
163
164                /// <summary>
165                /// Include memory region information. For more information, see
166                /// MINIDUMP_MEMORY_INFO_LIST.
167                /// </summary>
168                MiniDumpWithFullMemoryInfo = 0x00000800,
169
170                /// <summary>
171                /// Include thread state information. For more information, see
172                /// MINIDUMP_THREAD_INFO_LIST.
173                /// </summary>
174                MiniDumpWithThreadInfo = 0x00001000,
175
176                /// <summary>
177                /// Include all code and code-related sections from loaded modules to capture
178                /// executable content. For per-module control, use the ModuleWriteCodeSegs
179                /// enumeration value from MODULE_WRITE_FLAGS.
180                /// </summary>
181                MiniDumpWithCodeSegs = 0x00002000,
182
183                /// <summary>
184                /// Turns off secondary auxiliary-supported memory gathering.
185                /// </summary>
186                MiniDumpWithoutAuxiliaryState = 0x00004000,
187
188                /// <summary>
189                /// Requests that auxiliary data providers include their state in the dump
190                /// image; the state data that is included is provider dependent. This option
191                /// can result in a large dump image.
192                /// </summary>
193                MiniDumpWithFullAuxiliaryState = 0x00008000,
194
195                /// <summary>
196                /// Scans the virtual address space for PAGE_WRITECOPY memory to be included.
197                /// </summary>
198                MiniDumpWithPrivateWriteCopyMemory = 0x00010000,
199
200                /// <summary>
201                /// If you specify MiniDumpWithFullMemory, the MiniDumpWriteDump function will
202                /// fail if the function cannot read the memory regions; however, if you include
203                /// MiniDumpIgnoreInaccessibleMemory, the MiniDumpWriteDump function will
204                /// ignore the memory read failures and continue to generate the dump. Note that
205                /// the inaccessible memory regions are not included in the dump.
206                /// </summary>
207                MiniDumpIgnoreInaccessibleMemory = 0x00020000,
208
209                /// <summary>
210                /// Adds security token related data. This will make the "!token" extension work
211                /// when processing a user-mode dump.
212                /// </summary>
213                MiniDumpWithTokenInformation = 0x00040000
214            }
215
216            /// <summary>
217            /// Contains the exception information written to the minidump file by the
218            /// MiniDumpWriteDump function.
219            /// </summary>
220            [StructLayout(LayoutKind.Sequential, Pack = 4)]
221            public struct MiniDumpExceptionInfo
222            {
223                /// <summary>
224                /// The identifier of the thread throwing the exception.
225                /// </summary>
226                public uint ThreadId;
227
228                /// <summary>
229                ///  A pointer to an EXCEPTION_POINTERS structure specifying a
230                ///  computer-independent description of the exception and the processor
231                ///  context at the time of the exception.
232                /// </summary>
233                public IntPtr ExceptionPointers;
234
235                /// <summary>
236                /// Determines where to get the memory regions pointed to by the
237                /// ExceptionPointers member. Set to TRUE if the memory resides in the
238                /// process being debugged (the target process of the debugger). Otherwise,
239                /// set to FALSE if the memory resides in the address space of the calling
240                /// program (the debugger process). If you are accessing local memory (in
241                /// the calling process) you should not set this member to TRUE.
242                /// </summary>
243                [MarshalAs(UnmanagedType.Bool)]
244                public bool ClientPointers;
245            }
246        }
247
248        /// <summary>
249        /// Initialises the BlackBox handler. Call this initialiser once throughout
250        /// the lifespan of the application.
251        /// </summary>
252        /// <returns>The global BlackBox instance.</returns>
253        public static BlackBox Get()
254        {
255            if (Instance == null)
256                Instance = new BlackBox();
257            return Instance;
258        }
259
260        /// <summary>
261        /// Creates a new BlackBox report based on the exception provided.
262        /// </summary>
263        /// <param name="e">The exception which triggered this dump.</param>
264        public void CreateReport(Exception e)
265        {
266            if (e == null)
267                throw new ArgumentNullException("e");
268
269            //Generate a unique identifier for this report.
270            string crashName = DateTime.Now.ToString(CrashReportName, CultureInfo.InvariantCulture);
271            string currentCrashReport = Path.Combine(CrashReportsPath, crashName);
272            Directory.CreateDirectory(currentCrashReport);
273
274            //Then write a user-readable summary
275            WriteDebugLog(currentCrashReport, e);
276
277            //Take a screenshot
278            WriteScreenshot(currentCrashReport);
279
280            //Write a memory dump to the folder
281            WriteMemoryDump(currentCrashReport, e);
282        }
283
284        /// <summary>
285        /// Enumerates the list of crash dumps waiting for upload.
286        /// </summary>
287        /// <returns>A string array containing the list of dumps waiting for upload.</returns>
288        public BlackBoxReport[] GetDumps()
289        {
290            DirectoryInfo dirInfo = new DirectoryInfo(CrashReportsPath);
291            List<BlackBoxReport> result = new List<BlackBoxReport>();
292            if (dirInfo.Exists)
293                foreach (DirectoryInfo subDir in dirInfo.GetDirectories())
294                    result.Add(new BlackBoxReport(Path.Combine(CrashReportsPath, subDir.Name)));
295
296            return result.ToArray();
297        }
298
299        /// <summary>
300        /// Constructor. Use the <see cref="Initialise"/> function to use this class.
301        /// </summary>
302        private BlackBox()
303        {
304            AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
305            Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException);
306        }
307
308        /// <summary>
309        /// Called when an unhandled exception is raised in the application.
310        /// </summary>
311        private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
312        {
313            CreateReport(e.ExceptionObject as Exception);
314        }
315
316        /// <summary>
317        /// Dumps the contents of memory to a dumpfile.
318        /// </summary>
319        /// <param name="dumpFolder">Path to the folder to store the dump file.</param>
320        /// <param name="e">The exception which is being handled.</param>
321        private void WriteMemoryDump(string dumpFolder, Exception e)
322        {
323            //Open a file stream
324            using (FileStream stream = new FileStream(Path.Combine(dumpFolder, MemoryDumpFileName),
325                FileMode.OpenOrCreate, FileAccess.Write, FileShare.None))
326            {
327                //Store the exception information
328                NativeMethods.MiniDumpExceptionInfo exception =
329                    new NativeMethods.MiniDumpExceptionInfo();
330                exception.ClientPointers = false;
331                exception.ExceptionPointers = Marshal.GetExceptionPointers();
332                exception.ThreadId = (uint)AppDomain.GetCurrentThreadId();
333
334                NativeMethods.MiniDumpWriteDump(Process.GetCurrentProcess().Handle,
335                    (uint)Process.GetCurrentProcess().Id, stream.SafeFileHandle,
336                    NativeMethods.MiniDumpType.MiniDumpWithFullMemory,
337                    ref exception, IntPtr.Zero, IntPtr.Zero);
338            }
339        }
340
341        /// <summary>
342        /// Writes a debug log to the given directory.
343        /// </summary>
344        /// <param name="screenshotPath">The path to store the screenshot into.</param>
345        /// <param name="exception">The exception to log about.</param>
346        private void WriteDebugLog(string dumpFolder, Exception exception)
347        {
348            using (FileStream file = new FileStream(Path.Combine(dumpFolder, DebugLogFileName),
349                FileMode.OpenOrCreate, FileAccess.Write, FileShare.None))
350            using (StreamWriter stream = new StreamWriter(file))
351            {
352                //Application information
353                string separator = new string('-', 76);
354                string lineFormat = "{0,15}: {1}";
355                stream.WriteLine("Application Information");
356                stream.WriteLine(separator);
357                stream.WriteLine(string.Format(lineFormat, "Version",
358                    Assembly.GetEntryAssembly().GetName().Version));
359                StringBuilder commandLine = new StringBuilder();
360                foreach (string param in Environment.GetCommandLineArgs())
361                {
362                    commandLine.Append(param);
363                    commandLine.Append(' ');
364                }
365                stream.WriteLine(string.Format(lineFormat, "Command Line",
366                    commandLine.ToString().Trim()));
367
368                //Exception Information
369                stream.WriteLine();
370                stream.WriteLine("Exception Information (Outermost to innermost)");
371                stream.WriteLine(separator);
372
373                //Open a stream to the Stack Trace Log file. We want to separate the stack
374                //trace do we can check against the server to see if the crash is a new one
375                using (StreamWriter stackTraceLog = new StreamWriter(
376                    Path.Combine(dumpFolder, StackTraceFileName)))
377                {
378                    Exception currentException = exception;
379                    for (uint i = 1; currentException != null; ++i)
380                    {
381                        stream.WriteLine(string.Format("Exception {0}:", i));
382                        stream.WriteLine(string.Format(lineFormat, "Message", currentException.Message));
383                        stream.WriteLine(string.Format(lineFormat, "Exception Type",
384                            currentException.GetType().FullName));
385                        stackTraceLog.WriteLine(string.Format("Exception {0}: {1}", i,
386                            currentException.GetType().FullName));
387
388                        //Parse the stack trace
389                        string[] stackTrace = currentException.StackTrace.Split(new char[] { '\n' });
390                        for (uint j = 0; j < stackTrace.Length; ++j)
391                        {
392                            stream.WriteLine(string.Format(lineFormat,
393                                string.Format("Stack Trace [{0}]", j), stackTrace[j].Trim()));
394                            stackTraceLog.WriteLine(string.Format("{0}", stackTrace[j].Trim()));
395                        }
396
397                        uint k = 0;
398                        foreach (System.Collections.DictionaryEntry value in currentException.Data)
399                            stream.WriteLine(string.Format(lineFormat, string.Format("Data[{0}]", ++k),
400                                string.Format("{0} {1}", value.Key.ToString(), value.Value.ToString())));
401
402                        //End the exception and get the inner exception.
403                        stream.WriteLine();
404                        currentException = exception.InnerException;
405                    }
406                }
407            }
408        }
409
410        /// <summary>
411        /// Writes a screenshot to the given directory
412        /// </summary>
413        /// <param name="dumpFolder">The path to save the screenshot to.</param>
414        private void WriteScreenshot(string dumpFolder)
415        {
416            //Get the size of the screen
417            Rectangle rect = new Rectangle(int.MaxValue, int.MaxValue, int.MinValue, int.MinValue);
418            foreach (Screen screen in Screen.AllScreens)
419                rect = Rectangle.Union(rect, screen.Bounds);
420
421            //Copy a screen DC to the screenshot bitmap
422            Bitmap screenShot = new Bitmap(rect.Width, rect.Height);
423            Graphics bitmap = Graphics.FromImage(screenShot);
424            bitmap.CopyFromScreen(0, 0, 0, 0, rect.Size, CopyPixelOperation.SourceCopy);
425
426            //Save the bitmap to disk
427            screenShot.Save(Path.Combine(dumpFolder, ScreenshotFileName), ImageFormat.Png);
428        }
429
430        /// <summary>
431        /// The global BlackBox instance.
432        /// </summary>
433        private static BlackBox Instance;
434
435        /// <summary>
436        /// The path to all Eraser crash reports.
437        /// </summary>
438        private static readonly string CrashReportsPath = Path.Combine(Environment.GetFolderPath(
439            Environment.SpecialFolder.LocalApplicationData), @"Eraser 6\Crash Reports");
440
441        /// <summary>
442        /// The report name format.
443        /// </summary>
444        internal static readonly string CrashReportName = "yyyyMMdd HHmmss.FFF";
445
446        /// <summary>
447        /// The file name of the memory dump.
448        /// </summary>
449        ///
450        internal static readonly string MemoryDumpFileName = "Memory.dmp";
451
452        /// <summary>
453        /// The file name of the debug log.
454        /// </summary>
455        internal static readonly string DebugLogFileName = "Debug.log";
456
457        /// <summary>
458        /// The file name of the screenshot.
459        /// </summary>
460        internal static readonly string ScreenshotFileName = "Screenshot.png";
461
462        /// <summary>
463        /// The file name of the stack trace.
464        /// </summary>
465        internal static readonly string StackTraceFileName = "Stack Trace.log";
466    }
467
468    /// <summary>
469    /// Represents one BlackBox crash report.
470    /// </summary>
471    public class BlackBoxReport
472    {
473        /// <summary>
474        /// Constructor.
475        /// </summary>
476        /// <param name="path">Path to the folder containing the memory dump, screenshot and
477        /// debug log.</param>
478        internal BlackBoxReport(string path)
479        {
480            Path = path;
481
482            string[] stackTrace = null;
483            using (StreamReader reader = new StreamReader(
484                System.IO.Path.Combine(Path, BlackBox.StackTraceFileName)))
485            {
486                stackTrace = reader.ReadToEnd().Split(new char[] { '\n' });
487            }
488
489            //Parse the lines in the file.
490            StackTraceCache = new List<BlackBoxExceptionEntry>();
491            List<string> currentException = new List<string>();
492            string exceptionType = null;
493            foreach (string str in stackTrace)
494            {
495                if (str.StartsWith("Exception "))
496                {
497                    //Add the current exception to the list of exceptions.
498                    if (currentException.Count != 0)
499                    {
500                        StackTraceCache.Add(new BlackBoxExceptionEntry(exceptionType,
501                            new List<string>(currentException)));
502                        currentException.Clear();
503                    }
504
505                    //Set the exception type for the next exception.
506                    exceptionType = str.Substring(str.IndexOf(':') + 1).Trim();
507                }
508                else if (!string.IsNullOrEmpty(str.Trim()))
509                {
510                    currentException.Add(str.Trim());
511                }
512            }
513
514            if (currentException.Count != 0)
515                StackTraceCache.Add(new BlackBoxExceptionEntry(exceptionType, currentException));
516        }
517
518        /// <summary>
519        /// Deletes the report and its contents.
520        /// </summary>
521        public void Delete()
522        {
523            Directory.Delete(Path, true);
524        }
525
526        /// <summary>
527        /// The name of the report.
528        /// </summary>
529        public string Name
530        {
531            get
532            {
533                return System.IO.Path.GetFileName(Path);
534            }
535        }
536
537        /// <summary>
538        /// The timestamp of the report.
539        /// </summary>
540        public DateTime Timestamp
541        {
542            get
543            {
544                return DateTime.ParseExact(Name, BlackBox.CrashReportName,
545                    CultureInfo.InvariantCulture);
546            }
547        }
548
549        /// <summary>
550        /// The path to the folder containing the report.
551        /// </summary>
552        public string Path
553        {
554            get;
555            private set;
556        }
557
558        /// <summary>
559        /// The files which comprise the error report.
560        /// </summary>
561        public ReadOnlyCollection<FileInfo> Files
562        {
563            get
564            {
565                List<FileInfo> result = new List<FileInfo>();
566                DirectoryInfo directory = new DirectoryInfo(Path);
567                foreach (FileInfo file in directory.GetFiles())
568                    if (!InternalFiles.Contains(file.Name))
569                        result.Add(file);
570
571                return result.AsReadOnly();
572            }
573        }
574
575        /// <summary>
576        /// Gets a read-only stream which reads the Debug log.
577        /// </summary>
578        public Stream DebugLog
579        {
580            get
581            {
582                return new FileStream(System.IO.Path.Combine(Path, BlackBox.DebugLogFileName),
583                    FileMode.Open, FileAccess.Read);
584            }
585        }
586
587        /// <summary>
588        /// Gets the stack trace for this crash report.
589        /// </summary>
590        public ReadOnlyCollection<BlackBoxExceptionEntry> StackTrace
591        {
592            get
593            {
594                return StackTraceCache.AsReadOnly();
595            }
596        }
597
598        /// <summary>
599        /// Gets or sets whether the given report has been uploaded to the server.
600        /// </summary>
601        public bool Submitted
602        {
603            get
604            {
605                byte[] buffer = new byte[1];
606                using (FileStream stream = new FileStream(System.IO.Path.Combine(Path, StatusFileName),
607                    FileMode.OpenOrCreate, FileAccess.Read, FileShare.Read))
608                {
609                    stream.Read(buffer, 0, buffer.Length);
610                }
611
612                return buffer[0] == 1;
613            }
614
615            set
616            {
617                byte[] buffer = { Convert.ToByte(value) };
618                using (FileStream stream = new FileStream(System.IO.Path.Combine(Path, StatusFileName),
619                    FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read))
620                {
621                    stream.Write(buffer, 0, buffer.Length);
622                }
623            }
624        }
625
626        public override string ToString()
627        {
628            return Name;
629        }
630
631        /// <summary>
632        /// The backing variable for the <see cref="StackTrace"/> field.
633        /// </summary>
634        private List<BlackBoxExceptionEntry> StackTraceCache;
635
636        /// <summary>
637        /// The file name for the status file.
638        /// </summary>
639        private static readonly string StatusFileName = "Status.txt";
640
641        /// <summary>
642        /// The list of files internal to the report.
643        /// </summary>
644        private static readonly List<string> InternalFiles = new List<string>(
645            new string[] {
646                 BlackBox.StackTraceFileName,
647                 "Status.txt"
648            }
649        );
650    }
651
652    /// <summary>
653    /// Represents one exception which can be chained <see cref="InnerException"/>
654    /// to represent the exception handled by BlackBox
655    /// </summary>
656    public class BlackBoxExceptionEntry
657    {
658        /// <summary>
659        /// Constructor.
660        /// </summary>
661        /// <param name="exceptionType">The type of the exception.</param>
662        /// <param name="stackTrace">The stack trace for this exception.</param>
663        internal BlackBoxExceptionEntry(string exceptionType, List<string> stackTrace)
664        {
665            ExceptionType = exceptionType;
666            StackTraceCache = stackTrace;
667        }
668
669        /// <summary>
670        /// The type of the exception.
671        /// </summary>
672        public string ExceptionType
673        {
674            get;
675            private set;
676        }
677
678        /// <summary>
679        /// The stack trace for this exception.
680        /// </summary>
681        public ReadOnlyCollection<string> StackTrace
682        {
683            get
684            {
685                return StackTraceCache.AsReadOnly();
686            }
687        }
688
689        /// <summary>
690        /// The backing variable for the <see cref="StackTrace"/> property.
691        /// </summary>
692        private List<string> StackTraceCache;
693    }
694}
Note: See TracBrowser for help on using the repository browser.