source: trunk/eraser6/Eraser.Util/BlackBox.cs @ 1875

Revision 1875, 25.2 KB checked in by lowjoel, 5 years ago (diff)

Include the mouse cursor in the screenshot as it is useful for debugging UI bugs.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1/*
2 * $Id$
3 * Copyright 2008-2010 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.Collections.ObjectModel;
36using System.Globalization;
37
38using ICSharpCode.SharpZipLib.Tar;
39using ICSharpCode.SharpZipLib.BZip2;
40using System.Net;
41using System.Xml;
42using System.ComponentModel;
43
44namespace Eraser.Util
45{
46    /// <summary>
47    /// Handles application exceptions, stores minidumps and uploads them to the
48    /// Eraser server.
49    /// </summary>
50    public class BlackBox
51    {
52        /// <summary>
53        /// Initialises the BlackBox handler. Call this initialiser once throughout
54        /// the lifespan of the application.
55        /// </summary>
56        /// <returns>The global BlackBox instance.</returns>
57        public static BlackBox Get()
58        {
59            if (Instance == null)
60                Instance = new BlackBox();
61            return Instance;
62        }
63
64        /// <summary>
65        /// Creates a new BlackBox report based on the exception provided.
66        /// </summary>
67        /// <param name="e">The exception which triggered this dump.</param>
68        public void CreateReport(Exception e)
69        {
70            if (e == null)
71                throw new ArgumentNullException("e");
72
73            //Generate a unique identifier for this report.
74            string crashName = DateTime.Now.ToUniversalTime().ToString(
75                CrashReportName, CultureInfo.InvariantCulture);
76            string currentCrashReport = Path.Combine(CrashReportsPath, crashName);
77
78            //Create the report folder. If we can't create the report folder, we can't
79            //create the report contents.
80            Directory.CreateDirectory(currentCrashReport);
81            if (!Directory.Exists(currentCrashReport))
82                return;
83
84            //Store the steps which we have completed.
85            int currentStep = 0;
86
87            try
88            {
89                //First, write a user-readable summary
90                WriteDebugLog(currentCrashReport, e);
91                ++currentStep;
92
93                //Take a screenshot
94                WriteScreenshot(currentCrashReport);
95                ++currentStep;
96
97                //Write a memory dump to the folder
98                WriteMemoryDump(currentCrashReport, e);
99                ++currentStep;
100            }
101            catch
102            {
103                //If an exception was caught while creating the report, we should just
104                //abort as that may cause a cascade. However, we need to remove the
105                //report folder if the crash report is empty.
106                if (currentStep == 0)
107                    Directory.Delete(currentCrashReport);
108            }
109        }
110
111        /// <summary>
112        /// Enumerates the list of crash dumps waiting for upload.
113        /// </summary>
114        /// <returns>A string array containing the list of dumps waiting for upload.</returns>
115        public BlackBoxReport[] GetDumps()
116        {
117            DirectoryInfo dirInfo = new DirectoryInfo(CrashReportsPath);
118            List<BlackBoxReport> result = new List<BlackBoxReport>();
119            if (dirInfo.Exists)
120                foreach (DirectoryInfo subDir in dirInfo.GetDirectories())
121                    try
122                    {
123                        result.Add(new BlackBoxReport(Path.Combine(CrashReportsPath, subDir.Name)));
124                    }
125                    catch (InvalidDataException)
126                    {
127                        //Do nothing: invalid reports are automatically deleted.
128                    }
129
130            return result.ToArray();
131        }
132
133        /// <summary>
134        /// Constructor. Use the <see cref="Initialise"/> function to use this class.
135        /// </summary>
136        private BlackBox()
137        {
138            //If we have a debugger attached we shouldn't bother with exceptions.
139            if (Debugger.IsAttached)
140                return;
141
142            AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
143            Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException);
144        }
145
146        /// <summary>
147        /// Called when an unhandled exception is raised in the application.
148        /// </summary>
149        private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
150        {
151            CreateReport(e.ExceptionObject as Exception);
152        }
153
154        /// <summary>
155        /// Dumps the contents of memory to a dumpfile.
156        /// </summary>
157        /// <param name="dumpFolder">Path to the folder to store the dump file.</param>
158        /// <param name="e">The exception which is being handled.</param>
159        private void WriteMemoryDump(string dumpFolder, Exception e)
160        {
161            //Open a file stream
162            using (FileStream stream = new FileStream(Path.Combine(dumpFolder, MemoryDumpFileName),
163                FileMode.OpenOrCreate, FileAccess.Write, FileShare.None))
164            {
165                //Store the exception information
166                NativeMethods.MiniDumpExceptionInfo exception =
167                    new NativeMethods.MiniDumpExceptionInfo();
168                exception.ClientPointers = false;
169                exception.ExceptionPointers = Marshal.GetExceptionPointers();
170                exception.ThreadId = (uint)AppDomain.GetCurrentThreadId();
171
172                NativeMethods.MiniDumpWriteDump(Process.GetCurrentProcess().Handle,
173                    (uint)Process.GetCurrentProcess().Id, stream.SafeFileHandle,
174                    NativeMethods.MiniDumpType.MiniDumpWithFullMemory,
175                    ref exception, IntPtr.Zero, IntPtr.Zero);
176            }
177        }
178
179        /// <summary>
180        /// Writes a debug log to the given directory.
181        /// </summary>
182        /// <param name="screenshotPath">The path to store the screenshot into.</param>
183        /// <param name="exception">The exception to log about.</param>
184        private void WriteDebugLog(string dumpFolder, Exception exception)
185        {
186            using (FileStream file = new FileStream(Path.Combine(dumpFolder, DebugLogFileName),
187                FileMode.OpenOrCreate, FileAccess.Write, FileShare.None))
188            using (StreamWriter stream = new StreamWriter(file))
189            {
190                //Application information
191                string separator = new string('-', 76);
192                string lineFormat = "{0,17}: {1}";
193                stream.WriteLine("Application Information");
194                stream.WriteLine(separator);
195                stream.WriteLine(string.Format(lineFormat, "Version",
196                    Assembly.GetEntryAssembly().GetName().Version));
197                StringBuilder commandLine = new StringBuilder();
198                foreach (string param in Environment.GetCommandLineArgs())
199                {
200                    commandLine.Append(param);
201                    commandLine.Append(' ');
202                }
203                stream.WriteLine(string.Format(lineFormat, "Command Line",
204                    commandLine.ToString().Trim()));
205                stream.WriteLine(string.Format(lineFormat, "Current Directory",
206                    Environment.CurrentDirectory));
207
208                //System Information
209                stream.WriteLine();
210                stream.WriteLine("System Information");
211                stream.WriteLine(separator);
212                stream.WriteLine(string.Format(lineFormat, "Operating System",
213                    string.Format("{0} {2} {3}{1}",
214                        Environment.OSVersion.VersionString,
215                        string.IsNullOrEmpty(Environment.OSVersion.ServicePack) ?
216                            string.Empty :
217                            string.Format("(Service Pack {2})", Environment.OSVersion.ServicePack),
218                        SystemInfo.WindowsEdition == WindowsEditions.Undefined ?
219                            "" : SystemInfo.WindowsEdition.ToString(),
220                        SystemInfo.ProcessorArchitecture)));
221                stream.WriteLine(string.Format(lineFormat, "Processor Count",
222                    Environment.ProcessorCount));
223               
224                //Running processes
225                stream.WriteLine();
226                stream.WriteLine("Running Processes");
227                stream.WriteLine(separator);
228                {
229                    int i = 0;
230                    foreach (Process process in Process.GetProcesses())
231                    {
232                        try
233                        {
234                            ProcessModule mainModule = process.MainModule;
235                            stream.WriteLine(string.Format(lineFormat,
236                                string.Format("Process[{0}]", ++i),
237                                string.Format("{0} [{1}.{2}.{3}.{4}{5}]", mainModule.FileName,
238                                    mainModule.FileVersionInfo.FileMajorPart,
239                                    mainModule.FileVersionInfo.FileMinorPart,
240                                    mainModule.FileVersionInfo.FileBuildPart,
241                                    mainModule.FileVersionInfo.FilePrivatePart,
242                                    string.IsNullOrEmpty(mainModule.FileVersionInfo.FileVersion) ?
243                                        string.Empty :
244                                        string.Format(" <{0}>",
245                                            mainModule.FileVersionInfo.FileVersion))));
246                        }
247                        catch (Win32Exception)
248                        {
249                        }
250                    }
251                }
252
253                //Exception Information
254                stream.WriteLine();
255                stream.WriteLine("Exception Information (Outermost to innermost)");
256                stream.WriteLine(separator);
257
258                //Open a stream to the Stack Trace Log file. We want to separate the stack
259                //trace do we can check against the server to see if the crash is a new one
260                using (StreamWriter stackTraceLog = new StreamWriter(
261                    Path.Combine(dumpFolder, BlackBoxReport.StackTraceFileName)))
262                {
263                    Exception currentException = exception;
264                    for (uint i = 1; currentException != null; ++i)
265                    {
266                        stream.WriteLine(string.Format("Exception {0}:", i));
267                        stream.WriteLine(string.Format(lineFormat, "Message", currentException.Message));
268                        stream.WriteLine(string.Format(lineFormat, "Exception Type",
269                            currentException.GetType().FullName));
270                        stackTraceLog.WriteLine(string.Format("Exception {0}: {1}", i,
271                            currentException.GetType().FullName));
272
273                        //Parse the stack trace
274                        string[] stackTrace = currentException.StackTrace.Split(new char[] { '\n' });
275                        for (uint j = 0; j < stackTrace.Length; ++j)
276                        {
277                            stream.WriteLine(string.Format(lineFormat,
278                                string.Format("Stack Trace [{0}]", j), stackTrace[j].Trim()));
279                            stackTraceLog.WriteLine(string.Format("{0}", stackTrace[j].Trim()));
280                        }
281
282                        uint k = 0;
283                        foreach (System.Collections.DictionaryEntry value in currentException.Data)
284                            stream.WriteLine(string.Format(lineFormat, string.Format("Data[{0}]", ++k),
285                                string.Format("{0} {1}", value.Key.ToString(), value.Value.ToString())));
286
287                        //End the exception and get the inner exception.
288                        stream.WriteLine();
289                        currentException = currentException.InnerException;
290                    }
291                }
292            }
293        }
294
295        /// <summary>
296        /// Writes a screenshot to the given directory
297        /// </summary>
298        /// <param name="dumpFolder">The path to save the screenshot to.</param>
299        private void WriteScreenshot(string dumpFolder)
300        {
301            //Get the size of the screen
302            Rectangle rect = new Rectangle(int.MaxValue, int.MaxValue, int.MinValue, int.MinValue);
303            foreach (Screen screen in Screen.AllScreens)
304                rect = Rectangle.Union(rect, screen.Bounds);
305
306            //Copy a screen DC to the screenshot bitmap
307            Bitmap screenShot = new Bitmap(rect.Width, rect.Height);
308            Graphics bitmap = Graphics.FromImage(screenShot);
309            bitmap.CopyFromScreen(0, 0, 0, 0, rect.Size, CopyPixelOperation.SourceCopy);
310
311            //Place the mouse pointer
312            Cursor.Current.Draw(bitmap, new Rectangle(Cursor.Position, Cursor.Current.Size));
313
314            //Save the bitmap to disk
315            screenShot.Save(Path.Combine(dumpFolder, ScreenshotFileName), ImageFormat.Png);
316        }
317
318        /// <summary>
319        /// The global BlackBox instance.
320        /// </summary>
321        private static BlackBox Instance;
322
323        /// <summary>
324        /// The path to all Eraser crash reports.
325        /// </summary>
326        private static readonly string CrashReportsPath = Path.Combine(Environment.GetFolderPath(
327            Environment.SpecialFolder.LocalApplicationData), @"Eraser 6\Crash Reports");
328
329        /// <summary>
330        /// The report name format.
331        /// </summary>
332        internal static readonly string CrashReportName = "yyyyMMdd HHmmss.FFF";
333
334        /// <summary>
335        /// The file name of the memory dump.
336        /// </summary>
337        ///
338        internal static readonly string MemoryDumpFileName = "Memory.dmp";
339
340        /// <summary>
341        /// The file name of the debug log.
342        /// </summary>
343        internal static readonly string DebugLogFileName = "Debug.log";
344
345        /// <summary>
346        /// The file name of the screenshot.
347        /// </summary>
348        internal static readonly string ScreenshotFileName = "Screenshot.png";
349    }
350
351    /// <summary>
352    /// Represents one BlackBox crash report.
353    /// </summary>
354    public class BlackBoxReport
355    {
356        /// <summary>
357        /// Constructor.
358        /// </summary>
359        /// <param name="path">Path to the folder containing the memory dump, screenshot and
360        /// debug log.</param>
361        internal BlackBoxReport(string path)
362        {
363            Path = path;
364
365            string stackTracePath = System.IO.Path.Combine(Path, StackTraceFileName);
366            if (!File.Exists(stackTracePath))
367            {
368                Delete();
369                throw new InvalidDataException("The BlackBox report is corrupt.");
370            }
371
372            string[] stackTrace = null;
373            using (StreamReader reader = new StreamReader(stackTracePath))
374                stackTrace = reader.ReadToEnd().Split(new char[] { '\n' });
375
376            //Parse the lines in the file.
377            StackTraceCache = new List<BlackBoxExceptionEntry>();
378            List<string> currentException = new List<string>();
379            string exceptionType = null;
380            foreach (string str in stackTrace)
381            {
382                if (str.StartsWith("Exception "))
383                {
384                    //Add the current exception to the list of exceptions.
385                    if (currentException.Count != 0)
386                    {
387                        StackTraceCache.Add(new BlackBoxExceptionEntry(exceptionType,
388                            new List<string>(currentException)));
389                        currentException.Clear();
390                    }
391
392                    //Set the exception type for the next exception.
393                    exceptionType = str.Substring(str.IndexOf(':') + 1).Trim();
394                }
395                else if (!string.IsNullOrEmpty(str.Trim()))
396                {
397                    currentException.Add(str.Trim());
398                }
399            }
400
401            if (currentException.Count != 0)
402                StackTraceCache.Add(new BlackBoxExceptionEntry(exceptionType, currentException));
403        }
404
405        /// <summary>
406        /// Deletes the report and its contents.
407        /// </summary>
408        public void Delete()
409        {
410            Directory.Delete(Path, true);
411        }
412
413        /// <summary>
414        /// The name of the report.
415        /// </summary>
416        public string Name
417        {
418            get
419            {
420                return System.IO.Path.GetFileName(Path);
421            }
422        }
423
424        /// <summary>
425        /// The timestamp of the report.
426        /// </summary>
427        public DateTime Timestamp
428        {
429            get
430            {
431                return DateTime.ParseExact(Name, BlackBox.CrashReportName,
432                    CultureInfo.InvariantCulture).ToLocalTime();
433            }
434        }
435
436        /// <summary>
437        /// The path to the folder containing the report.
438        /// </summary>
439        public string Path
440        {
441            get;
442            private set;
443        }
444
445        /// <summary>
446        /// The files which comprise the error report.
447        /// </summary>
448        public ReadOnlyCollection<FileInfo> Files
449        {
450            get
451            {
452                List<FileInfo> result = new List<FileInfo>();
453                DirectoryInfo directory = new DirectoryInfo(Path);
454                foreach (FileInfo file in directory.GetFiles())
455                    if (!InternalFiles.Contains(file.Name))
456                        result.Add(file);
457
458                return result.AsReadOnly();
459            }
460        }
461
462        /// <summary>
463        /// Gets a read-only stream which reads the Debug log.
464        /// </summary>
465        public Stream DebugLog
466        {
467            get
468            {
469                return new FileStream(System.IO.Path.Combine(Path, BlackBox.DebugLogFileName),
470                    FileMode.Open, FileAccess.Read, FileShare.Read);
471            }
472        }
473
474        /// <summary>
475        /// Gets the stack trace for this crash report.
476        /// </summary>
477        public ReadOnlyCollection<BlackBoxExceptionEntry> StackTrace
478        {
479            get
480            {
481                return StackTraceCache.AsReadOnly();
482            }
483        }
484
485        /// <summary>
486        /// Gets or sets whether the given report has been uploaded to the server.
487        /// </summary>
488        public bool Submitted
489        {
490            get
491            {
492                byte[] buffer = new byte[1];
493                using (FileStream stream = new FileStream(System.IO.Path.Combine(Path, StatusFileName),
494                    FileMode.OpenOrCreate, FileAccess.Read, FileShare.Read))
495                {
496                    stream.Read(buffer, 0, buffer.Length);
497                }
498
499                return buffer[0] == 1;
500            }
501
502            set
503            {
504                byte[] buffer = { Convert.ToByte(value) };
505                using (FileStream stream = new FileStream(System.IO.Path.Combine(Path, StatusFileName),
506                    FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read))
507                {
508                    stream.Write(buffer, 0, buffer.Length);
509                }
510            }
511        }
512
513        public override string ToString()
514        {
515            return Name;
516        }
517
518        /// <summary>
519        /// The backing variable for the <see cref="StackTrace"/> field.
520        /// </summary>
521        private List<BlackBoxExceptionEntry> StackTraceCache;
522
523        /// <summary>
524        /// The file name for the status file.
525        /// </summary>
526        private static readonly string StatusFileName = "Status.txt";
527
528        /// <summary>
529        /// The file name of the stack trace.
530        /// </summary>
531        internal static readonly string StackTraceFileName = "Stack Trace.log";
532
533        /// <summary>
534        /// The list of files internal to the report.
535        /// </summary>
536        private static readonly List<string> InternalFiles = new List<string>(
537            new string[] {
538                 StackTraceFileName,
539                 StatusFileName
540            }
541        );
542    }
543
544    /// <summary>
545    /// Represents one exception which can be chained <see cref="InnerException"/>
546    /// to represent the exception handled by BlackBox
547    /// </summary>
548    public class BlackBoxExceptionEntry
549    {
550        /// <summary>
551        /// Constructor.
552        /// </summary>
553        /// <param name="exceptionType">The type of the exception.</param>
554        /// <param name="stackTrace">The stack trace for this exception.</param>
555        internal BlackBoxExceptionEntry(string exceptionType, List<string> stackTrace)
556        {
557            ExceptionType = exceptionType;
558            StackTraceCache = stackTrace;
559        }
560
561        /// <summary>
562        /// The type of the exception.
563        /// </summary>
564        public string ExceptionType
565        {
566            get;
567            private set;
568        }
569
570        /// <summary>
571        /// The stack trace for this exception.
572        /// </summary>
573        public ReadOnlyCollection<string> StackTrace
574        {
575            get
576            {
577                return StackTraceCache.AsReadOnly();
578            }
579        }
580
581        /// <summary>
582        /// The backing variable for the <see cref="StackTrace"/> property.
583        /// </summary>
584        private List<string> StackTraceCache;
585    }
586
587    /// <summary>
588    /// Uploads <see cref="BlackBoxReport"/>s to the Eraser server.
589    /// </summary>
590    public class BlackBoxReportUploader
591    {
592        /// <summary>
593        /// Constructor.
594        /// </summary>
595        /// <param name="report">The report to upload.</param>
596        public BlackBoxReportUploader(BlackBoxReport report)
597        {
598            Report = report;
599            if (!Directory.Exists(UploadTempDir))
600                Directory.CreateDirectory(UploadTempDir);
601
602            ReportBaseName = Path.Combine(UploadTempDir, Report.Name);
603        }
604
605        /// <summary>
606        /// Gets from the server based on the stack trace whether this report is
607        /// new.
608        /// </summary>
609        public bool IsNew
610        {
611            get
612            {
613                PostDataBuilder builder = new PostDataBuilder();
614                builder.AddPart(new PostDataField("action", "status"));
615                AddStackTraceToRequest(Report.StackTrace, builder);
616
617                WebRequest reportRequest = HttpWebRequest.Create(BlackBoxServer);
618                reportRequest.ContentType = builder.ContentType;
619                reportRequest.Method = "POST";
620                using (Stream formStream = builder.Stream)
621                {
622                    reportRequest.ContentLength = formStream.Length;
623                    using (Stream requestStream = reportRequest.GetRequestStream())
624                    {
625                        int lastRead = 0;
626                        byte[] buffer = new byte[32768];
627                        while ((lastRead = formStream.Read(buffer, 0, buffer.Length)) != 0)
628                            requestStream.Write(buffer, 0, lastRead);
629                    }
630                }
631
632                try
633                {
634                    HttpWebResponse response = reportRequest.GetResponse() as HttpWebResponse;
635                    using (Stream responseStream = response.GetResponseStream())
636                    {
637                        XmlReader reader = XmlReader.Create(responseStream);
638                        reader.ReadToFollowing("crashReport");
639                        string reportStatus = reader.GetAttribute("status");
640                        switch (reportStatus)
641                        {
642                            case "exists":
643                                Report.Submitted = true;
644                                return false;
645
646                            case "new":
647                                return true;
648
649                            default:
650                                throw new InvalidDataException(
651                                    "Unknown crash report server response.");
652                        }
653                    }
654                }
655                catch (WebException e)
656                {
657                    using (Stream responseStream = e.Response.GetResponseStream())
658                    {
659                        try
660                        {
661                            XmlReader reader = XmlReader.Create(responseStream);
662                            reader.ReadToFollowing("error");
663                            throw new InvalidDataException(string.Format(CultureInfo.CurrentCulture,
664                                "The server encountered a problem while processing the request: {0}",
665                                reader.ReadString()));
666                        }
667                        catch (XmlException)
668                        {
669                        }
670                    }
671
672                    throw new InvalidDataException(((HttpWebResponse)e.Response).StatusDescription);
673                }
674            }
675        }
676
677        /// <summary>
678        /// Compresses the report for uploading.
679        /// </summary>
680        /// <param name="progress">The <see cref="ProgressManager"/> instance that the
681        /// Upload function is using.</param>
682        /// <param name="progressChanged">The progress changed event handler that should
683        /// be called for upload progress updates.</param>
684        private void Compress(SteppedProgressManager progress,
685            ProgressChangedEventHandler progressChanged)
686        {
687            using (FileStream archiveStream = new FileStream(ReportBaseName + ".tar",
688                    FileMode.Create, FileAccess.Write))
689            {
690                //Add the report into a tar file
691                TarArchive archive = TarArchive.CreateOutputTarArchive(archiveStream);
692                foreach (FileInfo file in Report.Files)
693                {
694                    TarEntry entry = TarEntry.CreateEntryFromFile(file.FullName);
695                    entry.Name = Path.GetFileName(entry.Name);
696                    archive.WriteEntry(entry, false);
697                }
698                archive.Close();
699            }
700
701            ProgressManager step = new ProgressManager();
702            progress.Steps.Add(new SteppedProgressManagerStep(step, 0.5f, "Compressing"));
703            using (FileStream bzipFile = new FileStream(ReportBaseName + ".tbz",
704                FileMode.Create))
705            using (FileStream tarStream = new FileStream(ReportBaseName + ".tar",
706                FileMode.Open, FileAccess.Read, FileShare.Read, 262144, FileOptions.DeleteOnClose))
707            using (BZip2OutputStream bzipStream = new BZip2OutputStream(bzipFile, 262144))
708            {
709                //Compress the tar file
710                int lastRead = 0;
711                byte[] buffer = new byte[524288];
712                while ((lastRead = tarStream.Read(buffer, 0, buffer.Length)) != 0)
713                {
714                    bzipStream.Write(buffer, 0, lastRead);
715                    step.Total = tarStream.Length;
716                    step.Completed = tarStream.Position;
717
718                    if (progressChanged != null)
719                        progressChanged(this, new ProgressChangedEventArgs(progress, null));
720                }
721            }
722        }
723
724        /// <summary>
725        /// Compresses the report, then uploads it to the server.
726        /// </summary>
727        /// <param name="progressChanged">The progress changed event handler that should
728        /// be called for upload progress updates.</param>
729        public void Submit(ProgressChangedEventHandler progressChanged)
730        {
731            SteppedProgressManager overallProgress = new SteppedProgressManager();
732            Compress(overallProgress, progressChanged);
733
734            using (FileStream bzipFile = new FileStream(ReportBaseName + ".tbz",
735                FileMode.Open, FileAccess.Read, FileShare.Read, 131072, FileOptions.DeleteOnClose))
736            using (Stream logFile = Report.DebugLog)
737            {
738                //Build the POST request
739                PostDataBuilder builder = new PostDataBuilder();
740                builder.AddPart(new PostDataField("action", "upload"));
741                builder.AddPart(new PostDataFileField("crashReport", "Report.tbz", bzipFile));
742                AddStackTraceToRequest(Report.StackTrace, builder);
743
744                //Upload the POST request
745                WebRequest reportRequest = HttpWebRequest.Create(BlackBoxServer);
746                reportRequest.ContentType = builder.ContentType;
747                reportRequest.Method = "POST";
748                reportRequest.Timeout = int.MaxValue;
749                using (Stream formStream = builder.Stream)
750                {
751                    ProgressManager progress = new ProgressManager();
752                    overallProgress.Steps.Add(new SteppedProgressManagerStep(
753                        progress, 0.5f, "Uploading"));
754                    reportRequest.ContentLength = formStream.Length;
755
756                    using (Stream requestStream = reportRequest.GetRequestStream())
757                    {
758                        int lastRead = 0;
759                        byte[] buffer = new byte[32768];
760                        while ((lastRead = formStream.Read(buffer, 0, buffer.Length)) != 0)
761                        {
762                            requestStream.Write(buffer, 0, lastRead);
763
764                            progress.Total = formStream.Length;
765                            progress.Completed = formStream.Position;
766                            progressChanged(this, new ProgressChangedEventArgs(overallProgress, null));
767                        }
768                    }
769                }
770
771                try
772                {
773                    reportRequest.GetResponse();
774                    Report.Submitted = true;
775                }
776                catch (WebException e)
777                {
778                    using (Stream responseStream = e.Response.GetResponseStream())
779                    {
780                        try
781                        {
782                            XmlReader reader = XmlReader.Create(responseStream);
783                            reader.ReadToFollowing("error");
784                            throw new InvalidDataException(string.Format(CultureInfo.CurrentCulture,
785                                "The server encountered a problem while processing the request: {0}",
786                                reader.ReadString()));
787                        }
788                        catch (XmlException)
789                        {
790                        }
791                    }
792
793                    throw new InvalidDataException(((HttpWebResponse)e.Response).StatusDescription);
794                }
795            }
796        }
797
798        /// <summary>
799        /// Adds the stack trace to the given form request.
800        /// </summary>
801        /// <param name="stackTrace">The stack trace to add.</param>
802        /// <param name="builder">The Form request builder to add the stack trace to.</param>
803        private static void AddStackTraceToRequest(IList<BlackBoxExceptionEntry> stackTrace,
804            PostDataBuilder builder)
805        {
806            int exceptionIndex = 0;
807            foreach (BlackBoxExceptionEntry exceptionStack in stackTrace)
808            {
809                foreach (string stackFrame in exceptionStack.StackTrace)
810                    builder.AddPart(new PostDataField(
811                        string.Format("stackTrace[{0}][]", exceptionIndex), stackFrame));
812                builder.AddPart(new PostDataField(string.Format(
813                    "stackTrace[{0}][exception]", exceptionIndex), exceptionStack.ExceptionType));
814                ++exceptionIndex;
815            }
816        }
817
818        /// <summary>
819        /// The path to where the temporary files are stored before uploading.
820        /// </summary>
821        private static readonly string UploadTempDir =
822            Path.Combine(Path.GetTempPath(), "Eraser Crash Reports");
823
824        /// <summary>
825        /// The URI to the BlackBox server.
826        /// </summary>
827        private static readonly Uri BlackBoxServer =
828            new Uri("http://eraser.heidi.ie/scripts/blackbox/upload.php");
829
830        /// <summary>
831        /// The report being uploaded.
832        /// </summary>
833        private BlackBoxReport Report;
834
835        /// <summary>
836        /// The base name of the report.
837        /// </summary>
838        private readonly string ReportBaseName;
839    }
840}
Note: See TracBrowser for help on using the repository browser.