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

Revision 1454, 22.0 KB checked in by lowjoel, 5 years ago (diff)

Allow code to create crash dumps any time, not just when an unhandled exception is caught in the AppDomain?

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