source: trunk/eraser/Eraser.BlackBox/BlackBox.cs @ 2536

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