source: branches/eraser6/CodeReview/Eraser.Util/BlackBox.cs @ 1614

Revision 1614, 14.9 KB checked in by lowjoel, 5 years ago (diff)

Split the Eraser.Util.BlackBox?.NativeMethods? class to the Eraser.Util.NativeMethods? partial class. Addresses #317: Merge the BlackBox? branch

  • 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;
25
26using System.Windows.Forms;
27using System.IO;
28using System.Runtime.InteropServices;
29using System.Diagnostics;
30using System.Reflection;
31using Microsoft.Win32.SafeHandles;
32
33using System.Drawing;
34using System.Drawing.Imaging;
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        /// Initialises the BlackBox handler. Call this initialiser once throughout
47        /// the lifespan of the application.
48        /// </summary>
49        /// <returns>The global BlackBox instance.</returns>
50        public static BlackBox Get()
51        {
52            if (Instance == null)
53                Instance = new BlackBox();
54            return Instance;
55        }
56
57        /// <summary>
58        /// Creates a new BlackBox report based on the exception provided.
59        /// </summary>
60        /// <param name="e">The exception which triggered this dump.</param>
61        public void CreateReport(Exception e)
62        {
63            if (e == null)
64                throw new ArgumentNullException("e");
65
66            //Generate a unique identifier for this report.
67            string crashName = DateTime.Now.ToUniversalTime().ToString(
68                CrashReportName, CultureInfo.InvariantCulture);
69            string currentCrashReport = Path.Combine(CrashReportsPath, crashName);
70            Directory.CreateDirectory(currentCrashReport);
71
72            //Store the steps which we have completed.
73            int currentStep = 0;
74
75            try
76            {
77                //First, write a user-readable summary
78                WriteDebugLog(currentCrashReport, e);
79                ++currentStep;
80
81                //Take a screenshot
82                WriteScreenshot(currentCrashReport);
83                ++currentStep;
84
85                //Write a memory dump to the folder
86                WriteMemoryDump(currentCrashReport, e);
87                ++currentStep;
88            }
89            catch
90            {
91                //If an exception was caught while creating the report, we should just
92                //abort as that may cause a cascade. However, we need to remove the
93                //report folder if the crash report is empty.
94                if (currentStep == 0)
95                    Directory.Delete(currentCrashReport);
96            }
97        }
98
99        /// <summary>
100        /// Enumerates the list of crash dumps waiting for upload.
101        /// </summary>
102        /// <returns>A string array containing the list of dumps waiting for upload.</returns>
103        public BlackBoxReport[] GetDumps()
104        {
105            DirectoryInfo dirInfo = new DirectoryInfo(CrashReportsPath);
106            List<BlackBoxReport> result = new List<BlackBoxReport>();
107            if (dirInfo.Exists)
108                foreach (DirectoryInfo subDir in dirInfo.GetDirectories())
109                    try
110                    {
111                        result.Add(new BlackBoxReport(Path.Combine(CrashReportsPath, subDir.Name)));
112                    }
113                    catch (InvalidDataException)
114                    {
115                        //Do nothing: invalid reports are automatically deleted.
116                    }
117
118            return result.ToArray();
119        }
120
121        /// <summary>
122        /// Constructor. Use the <see cref="Initialise"/> function to use this class.
123        /// </summary>
124        private BlackBox()
125        {
126            AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
127            Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException);
128        }
129
130        /// <summary>
131        /// Called when an unhandled exception is raised in the application.
132        /// </summary>
133        private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
134        {
135            CreateReport(e.ExceptionObject as Exception);
136        }
137
138        /// <summary>
139        /// Dumps the contents of memory to a dumpfile.
140        /// </summary>
141        /// <param name="dumpFolder">Path to the folder to store the dump file.</param>
142        /// <param name="e">The exception which is being handled.</param>
143        private void WriteMemoryDump(string dumpFolder, Exception e)
144        {
145            //Open a file stream
146            using (FileStream stream = new FileStream(Path.Combine(dumpFolder, MemoryDumpFileName),
147                FileMode.OpenOrCreate, FileAccess.Write, FileShare.None))
148            {
149                //Store the exception information
150                NativeMethods.MiniDumpExceptionInfo exception =
151                    new NativeMethods.MiniDumpExceptionInfo();
152                exception.ClientPointers = false;
153                exception.ExceptionPointers = Marshal.GetExceptionPointers();
154                exception.ThreadId = (uint)AppDomain.GetCurrentThreadId();
155
156                NativeMethods.MiniDumpWriteDump(Process.GetCurrentProcess().Handle,
157                    (uint)Process.GetCurrentProcess().Id, stream.SafeFileHandle,
158                    NativeMethods.MiniDumpType.MiniDumpWithFullMemory,
159                    ref exception, IntPtr.Zero, IntPtr.Zero);
160            }
161        }
162
163        /// <summary>
164        /// Writes a debug log to the given directory.
165        /// </summary>
166        /// <param name="screenshotPath">The path to store the screenshot into.</param>
167        /// <param name="exception">The exception to log about.</param>
168        private void WriteDebugLog(string dumpFolder, Exception exception)
169        {
170            using (FileStream file = new FileStream(Path.Combine(dumpFolder, DebugLogFileName),
171                FileMode.OpenOrCreate, FileAccess.Write, FileShare.None))
172            using (StreamWriter stream = new StreamWriter(file))
173            {
174                //Application information
175                string separator = new string('-', 76);
176                string lineFormat = "{0,15}: {1}";
177                stream.WriteLine("Application Information");
178                stream.WriteLine(separator);
179                stream.WriteLine(string.Format(lineFormat, "Version",
180                    Assembly.GetEntryAssembly().GetName().Version));
181                StringBuilder commandLine = new StringBuilder();
182                foreach (string param in Environment.GetCommandLineArgs())
183                {
184                    commandLine.Append(param);
185                    commandLine.Append(' ');
186                }
187                stream.WriteLine(string.Format(lineFormat, "Command Line",
188                    commandLine.ToString().Trim()));
189
190                //Exception Information
191                stream.WriteLine();
192                stream.WriteLine("Exception Information (Outermost to innermost)");
193                stream.WriteLine(separator);
194
195                //Open a stream to the Stack Trace Log file. We want to separate the stack
196                //trace do we can check against the server to see if the crash is a new one
197                using (StreamWriter stackTraceLog = new StreamWriter(
198                    Path.Combine(dumpFolder, BlackBoxReport.StackTraceFileName)))
199                {
200                    Exception currentException = exception;
201                    for (uint i = 1; currentException != null; ++i)
202                    {
203                        stream.WriteLine(string.Format("Exception {0}:", i));
204                        stream.WriteLine(string.Format(lineFormat, "Message", currentException.Message));
205                        stream.WriteLine(string.Format(lineFormat, "Exception Type",
206                            currentException.GetType().FullName));
207                        stackTraceLog.WriteLine(string.Format("Exception {0}: {1}", i,
208                            currentException.GetType().FullName));
209
210                        //Parse the stack trace
211                        string[] stackTrace = currentException.StackTrace.Split(new char[] { '\n' });
212                        for (uint j = 0; j < stackTrace.Length; ++j)
213                        {
214                            stream.WriteLine(string.Format(lineFormat,
215                                string.Format("Stack Trace [{0}]", j), stackTrace[j].Trim()));
216                            stackTraceLog.WriteLine(string.Format("{0}", stackTrace[j].Trim()));
217                        }
218
219                        uint k = 0;
220                        foreach (System.Collections.DictionaryEntry value in currentException.Data)
221                            stream.WriteLine(string.Format(lineFormat, string.Format("Data[{0}]", ++k),
222                                string.Format("{0} {1}", value.Key.ToString(), value.Value.ToString())));
223
224                        //End the exception and get the inner exception.
225                        stream.WriteLine();
226                        currentException = exception.InnerException;
227                    }
228                }
229            }
230        }
231
232        /// <summary>
233        /// Writes a screenshot to the given directory
234        /// </summary>
235        /// <param name="dumpFolder">The path to save the screenshot to.</param>
236        private void WriteScreenshot(string dumpFolder)
237        {
238            //Get the size of the screen
239            Rectangle rect = new Rectangle(int.MaxValue, int.MaxValue, int.MinValue, int.MinValue);
240            foreach (Screen screen in Screen.AllScreens)
241                rect = Rectangle.Union(rect, screen.Bounds);
242
243            //Copy a screen DC to the screenshot bitmap
244            Bitmap screenShot = new Bitmap(rect.Width, rect.Height);
245            Graphics bitmap = Graphics.FromImage(screenShot);
246            bitmap.CopyFromScreen(0, 0, 0, 0, rect.Size, CopyPixelOperation.SourceCopy);
247
248            //Save the bitmap to disk
249            screenShot.Save(Path.Combine(dumpFolder, ScreenshotFileName), ImageFormat.Png);
250        }
251
252        /// <summary>
253        /// The global BlackBox instance.
254        /// </summary>
255        private static BlackBox Instance;
256
257        /// <summary>
258        /// The path to all Eraser crash reports.
259        /// </summary>
260        private static readonly string CrashReportsPath = Path.Combine(Environment.GetFolderPath(
261            Environment.SpecialFolder.LocalApplicationData), @"Eraser 6\Crash Reports");
262
263        /// <summary>
264        /// The report name format.
265        /// </summary>
266        internal static readonly string CrashReportName = "yyyyMMdd HHmmss.FFF";
267
268        /// <summary>
269        /// The file name of the memory dump.
270        /// </summary>
271        ///
272        internal static readonly string MemoryDumpFileName = "Memory.dmp";
273
274        /// <summary>
275        /// The file name of the debug log.
276        /// </summary>
277        internal static readonly string DebugLogFileName = "Debug.log";
278
279        /// <summary>
280        /// The file name of the screenshot.
281        /// </summary>
282        internal static readonly string ScreenshotFileName = "Screenshot.png";
283    }
284
285    /// <summary>
286    /// Represents one BlackBox crash report.
287    /// </summary>
288    public class BlackBoxReport
289    {
290        /// <summary>
291        /// Constructor.
292        /// </summary>
293        /// <param name="path">Path to the folder containing the memory dump, screenshot and
294        /// debug log.</param>
295        internal BlackBoxReport(string path)
296        {
297            Path = path;
298
299            string stackTracePath = System.IO.Path.Combine(Path, StackTraceFileName);
300            if (!System.IO.File.Exists(stackTracePath))
301            {
302                Delete();
303                throw new InvalidDataException("The BlackBox report is corrupt.");
304            }
305
306            string[] stackTrace = null;
307            using (StreamReader reader = new StreamReader(stackTracePath))
308                stackTrace = reader.ReadToEnd().Split(new char[] { '\n' });
309
310            //Parse the lines in the file.
311            StackTraceCache = new List<BlackBoxExceptionEntry>();
312            List<string> currentException = new List<string>();
313            string exceptionType = null;
314            foreach (string str in stackTrace)
315            {
316                if (str.StartsWith("Exception "))
317                {
318                    //Add the current exception to the list of exceptions.
319                    if (currentException.Count != 0)
320                    {
321                        StackTraceCache.Add(new BlackBoxExceptionEntry(exceptionType,
322                            new List<string>(currentException)));
323                        currentException.Clear();
324                    }
325
326                    //Set the exception type for the next exception.
327                    exceptionType = str.Substring(str.IndexOf(':') + 1).Trim();
328                }
329                else if (!string.IsNullOrEmpty(str.Trim()))
330                {
331                    currentException.Add(str.Trim());
332                }
333            }
334
335            if (currentException.Count != 0)
336                StackTraceCache.Add(new BlackBoxExceptionEntry(exceptionType, currentException));
337        }
338
339        /// <summary>
340        /// Deletes the report and its contents.
341        /// </summary>
342        public void Delete()
343        {
344            Directory.Delete(Path, true);
345        }
346
347        /// <summary>
348        /// The name of the report.
349        /// </summary>
350        public string Name
351        {
352            get
353            {
354                return System.IO.Path.GetFileName(Path);
355            }
356        }
357
358        /// <summary>
359        /// The timestamp of the report.
360        /// </summary>
361        public DateTime Timestamp
362        {
363            get
364            {
365                return DateTime.ParseExact(Name, BlackBox.CrashReportName,
366                    CultureInfo.InvariantCulture).ToLocalTime();
367            }
368        }
369
370        /// <summary>
371        /// The path to the folder containing the report.
372        /// </summary>
373        public string Path
374        {
375            get;
376            private set;
377        }
378
379        /// <summary>
380        /// The files which comprise the error report.
381        /// </summary>
382        public ReadOnlyCollection<FileInfo> Files
383        {
384            get
385            {
386                List<FileInfo> result = new List<FileInfo>();
387                DirectoryInfo directory = new DirectoryInfo(Path);
388                foreach (FileInfo file in directory.GetFiles())
389                    if (!InternalFiles.Contains(file.Name))
390                        result.Add(file);
391
392                return result.AsReadOnly();
393            }
394        }
395
396        /// <summary>
397        /// Gets a read-only stream which reads the Debug log.
398        /// </summary>
399        public Stream DebugLog
400        {
401            get
402            {
403                return new FileStream(System.IO.Path.Combine(Path, BlackBox.DebugLogFileName),
404                    FileMode.Open, FileAccess.Read);
405            }
406        }
407
408        /// <summary>
409        /// Gets the stack trace for this crash report.
410        /// </summary>
411        public ReadOnlyCollection<BlackBoxExceptionEntry> StackTrace
412        {
413            get
414            {
415                return StackTraceCache.AsReadOnly();
416            }
417        }
418
419        /// <summary>
420        /// Gets or sets whether the given report has been uploaded to the server.
421        /// </summary>
422        public bool Submitted
423        {
424            get
425            {
426                byte[] buffer = new byte[1];
427                using (FileStream stream = new FileStream(System.IO.Path.Combine(Path, StatusFileName),
428                    FileMode.OpenOrCreate, FileAccess.Read, FileShare.Read))
429                {
430                    stream.Read(buffer, 0, buffer.Length);
431                }
432
433                return buffer[0] == 1;
434            }
435
436            set
437            {
438                byte[] buffer = { Convert.ToByte(value) };
439                using (FileStream stream = new FileStream(System.IO.Path.Combine(Path, StatusFileName),
440                    FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read))
441                {
442                    stream.Write(buffer, 0, buffer.Length);
443                }
444            }
445        }
446
447        public override string ToString()
448        {
449            return Name;
450        }
451
452        /// <summary>
453        /// The backing variable for the <see cref="StackTrace"/> field.
454        /// </summary>
455        private List<BlackBoxExceptionEntry> StackTraceCache;
456
457        /// <summary>
458        /// The file name for the status file.
459        /// </summary>
460        private static readonly string StatusFileName = "Status.txt";
461
462        /// <summary>
463        /// The file name of the stack trace.
464        /// </summary>
465        internal static readonly string StackTraceFileName = "Stack Trace.log";
466
467        /// <summary>
468        /// The list of files internal to the report.
469        /// </summary>
470        private static readonly List<string> InternalFiles = new List<string>(
471            new string[] {
472                 StackTraceFileName,
473                 StatusFileName
474            }
475        );
476    }
477
478    /// <summary>
479    /// Represents one exception which can be chained <see cref="InnerException"/>
480    /// to represent the exception handled by BlackBox
481    /// </summary>
482    public class BlackBoxExceptionEntry
483    {
484        /// <summary>
485        /// Constructor.
486        /// </summary>
487        /// <param name="exceptionType">The type of the exception.</param>
488        /// <param name="stackTrace">The stack trace for this exception.</param>
489        internal BlackBoxExceptionEntry(string exceptionType, List<string> stackTrace)
490        {
491            ExceptionType = exceptionType;
492            StackTraceCache = stackTrace;
493        }
494
495        /// <summary>
496        /// The type of the exception.
497        /// </summary>
498        public string ExceptionType
499        {
500            get;
501            private set;
502        }
503
504        /// <summary>
505        /// The stack trace for this exception.
506        /// </summary>
507        public ReadOnlyCollection<string> StackTrace
508        {
509            get
510            {
511                return StackTraceCache.AsReadOnly();
512            }
513        }
514
515        /// <summary>
516        /// The backing variable for the <see cref="StackTrace"/> property.
517        /// </summary>
518        private List<string> StackTraceCache;
519    }
520}
Note: See TracBrowser for help on using the repository browser.