// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. using System; using System.Threading; using System.Windows.Forms; using System.IO; using System.Runtime.InteropServices; namespace AutoReporter { class OutputLogFile { private StreamWriter MyStreamWriter; private string FileName; public OutputLogFile(string filename) { try { MyStreamWriter = new StreamWriter(filename); this.FileName = filename; } catch(Exception) { MyStreamWriter = null; } } public void WriteLine(string line) { if(MyStreamWriter != null) { try { MyStreamWriter.WriteLine(line); MyStreamWriter.Flush(); } catch(Exception) { } } #if DEBUG Console.WriteLine(line); #endif } public void Close() { if(MyStreamWriter != null) { MyStreamWriter.Close(); } } }; static class Program { private static EpicBalloon NotifyObject = null; private const int BalloonTimeInMs = 2000; /** * Code for calling SendMessage. */ #region Win32 glue // import the SendMessage function so we can send windows messages to the UnrealConsole [DllImport("user32", CharSet = CharSet.Auto)] private static extern int SendMessage(HandleRef hWnd, int msg, int wParam, int lParam); [DllImport("user32", CharSet = CharSet.Auto)] private static extern int RegisterWindowMessage(string lpString); // Constants from the Platform SDK. private const int HWND_BROADCAST = 0xffff; #endregion /** * AutoReporter is a program for sending crash data to a web service. * First it opens a log file on the local machine. Then it parses the crash dump. * Next the ReportService is used to create a new report from data extracted from the crash dump. * The log file and ini dump are uploaded to the server. If all this succeeds, the user is prompted * to enter a description of the crash, which is then sent to the ReportService. * Finally, if no errors occur the dumps and log are deleted. * * 3 arguments expected: AutoReport Dump file name, Log file name and Ini dump file name */ [STAThread] static void Main (string[] args) { Application.EnableVisualStyles (); Application.SetCompatibleTextRenderingDefault (false); string logFileName; if (args.Length >= 2) { string logDirectory; int endOfLogPath = args [2].LastIndexOf ('/'); logDirectory = args [2].Substring (0, endOfLogPath + 1); logFileName = logDirectory + "AutoReportLog.txt"; } else { logFileName = "AutoReportLog.txt"; } OutputLogFile LogFile = new OutputLogFile (logFileName); LogFile.WriteLine ("Log opened: " + logFileName); LogFile.WriteLine (""); LogFile.WriteLine ("Current Time = " + DateTime.Now.ToString ()); LogFile.WriteLine (""); LogFile.WriteLine ("Arguments:"); foreach (string arg in args) { LogFile.WriteLine (arg); } if (args.Length < 6) { LogFile.WriteLine ("Expected 5 arguments: AutoReport Dump file name, Log file name, Ini dump file name, Mini dump file name, Crash video file name and ProcessID"); LogFile.Close (); return; } Int32 ProcessId = Convert.ToInt32 (args [0]); Thread.Sleep (1000); // Check for additional options bool bForceUnattended = false; bool bShowBalloon = false; for (int ExtraArgIndex = 6; ExtraArgIndex < args.Length; ++ExtraArgIndex) { string CurArgString = args [ExtraArgIndex]; // -Unattended : forces unattended mode regardless of command line string if (CurArgString.Equals ("-Unattended", StringComparison.OrdinalIgnoreCase)) { bForceUnattended = true; } // -Balloon : displays a system tray notify icon (balloon) and forces unattended mode else if (CurArgString.Equals ("-Balloon", StringComparison.OrdinalIgnoreCase)) { // balloon not possible on Mac if(Environment.OSVersion.Platform != PlatformID.Unix) bShowBalloon = true; // Unattended mode is implied with -Balloon bForceUnattended = true; } else { LogFile.WriteLine (String.Format ("Unrecognized parameter: {0}", CurArgString)); LogFile.Close (); return; } } ReportFile rFile = new ReportFile (); ReportFileData reportData = new ReportFileData (); LogFile.WriteLine ("Parsing report file: " + args [1]); if (!rFile.ParseReportFile (args [1], reportData, LogFile)) { LogFile.WriteLine ("Failed to parse report file: " + args [1]); LogFile.Close (); return; } string ProcessName = reportData.GameName; Int32 SpaceIndex = ProcessName.IndexOf (' '); if (SpaceIndex != -1) { // This is likely UE4 UDK... parse off the game name ProcessName = ProcessName.Substring (0, SpaceIndex); } bool bIsUnattended = reportData.CommandLine.Contains ("unattended") || bForceUnattended || Environment.OSVersion.Platform == PlatformID.Unix; string CrashDescription; string Summary = CrashDescription = bShowBalloon ? "Handled error" : "Unattended mode"; if (bShowBalloon) { String MsgText = "An unexpected error has occurred but the application has recovered. A report will be submitted to the QA database."; // If the error occurred in the editor then we'll remind them to save their changes if (reportData.EngineMode.Contains("Editor")) { MsgText += " Remember to save your work often."; } // Notify the user of the error by launching the NotifyObject. NotifyObject = new EpicBalloon(); NotifyObject.Show(MsgText, BalloonTimeInMs); } if (!bIsUnattended) { LogFile.WriteLine ("Attempting to create a new crash description form..."); Form1 crashDescForm = new Form1 (); crashDescForm.summary = Summary; crashDescForm.crashDesc = CrashDescription; LogFile.WriteLine ("Running attended..."); crashDescForm.SetCallStack (reportData.CallStack); LogFile.WriteLine ("Running the application..."); Application.Run (crashDescForm); Summary = crashDescForm.summary; CrashDescription = crashDescForm.crashDesc; } LogFile.WriteLine ("Crash Summary = " + Summary); LogFile.WriteLine ("Crash Description = " + CrashDescription); LogFile.WriteLine ("Registering report service..."); // Create an instance of AutoReportService handler to make a web request to the AutoReportService to create a Crash Report in the database. Once this is complete we'll upload the associated files. ReportService.RegisterReport reportService = new ReportService.RegisterReport (); Exception serviceException = new Exception (""); bool serviceError = false; LogFile.WriteLine ("Attempting to create a new crash..."); int uniqueIndex = -1; // Make the web request try { uniqueIndex = reportService.CreateNewCrash (-1, reportData.ComputerName, reportData.UserName, reportData.GameName, reportData.PlatformName, reportData.LanguageExt, reportData.TimeOfCrash, reportData.BuildVer, reportData.ChangelistVer, reportData.CommandLine, reportData.BaseDir, reportData.CallStack, reportData.EngineMode); } catch (Exception e) { LogFile.WriteLine ("AutoReporter had an exception in accessing the reportService! --> " + e.ToString ()); serviceException = e; serviceError = true; LogFile.WriteLine (e.Message); } LogFile.WriteLine (""); LogFile.WriteLine ("uniqueIndex = " + uniqueIndex.ToString ()); LogFile.WriteLine (""); if (uniqueIndex == -1) { LogFile.WriteLine ("The service failed to create a new Crash!"); serviceError = true; serviceException = new Exception ("The service failed to create a new Crash!"); } LogFile.WriteLine ("Attempting to create a new UpdateReportFiles instance..."); // Create handler to upload the files. UploadReportFiles reportUploader = new UploadReportFiles (); bool fileUploadSuccess = false; if (!serviceError) { LogFile.WriteLine ("Attempting to upload files..."); try { fileUploadSuccess = reportUploader.UploadFiles (ProcessName, bShowBalloon, ProcessId, args [2], args [3], args [4], args [5], uniqueIndex, LogFile); } catch (NullReferenceException nre) { LogFile.WriteLine (nre.Source + " Caused Null Reference Exception"); // Try again: try { LogFile.WriteLine ("Process Name: " + ProcessName); LogFile.WriteLine ("bShowBalloon: " + bShowBalloon); LogFile.WriteLine ("ProcessId: " + ProcessId); LogFile.WriteLine ("args[2]: " + args [2]); LogFile.WriteLine ("args[3]: " + args [3]); LogFile.WriteLine ("args[4]: " + args [4]); LogFile.WriteLine ("args[5] ; " + args [5]); LogFile.WriteLine ("uniqueIndex " + uniqueIndex); LogFile.WriteLine ("LogFile " + LogFile); fileUploadSuccess = reportUploader.UploadFiles (ProcessName, bShowBalloon, ProcessId, args [2], args [3], args [4], args [5], uniqueIndex, LogFile); } catch (NullReferenceException nre2) { LogFile.WriteLine (nre2.Source + " Caused Null Reference Exception Two Times"); serviceError = true; } } catch (Exception e) { LogFile.WriteLine ("AutoReporter had an exception uploading files! --> " + e.ToString ()); serviceException = e; serviceError = true; } if (fileUploadSuccess) { LogFile.WriteLine ("Successfully uploaded files"); LogFile.WriteLine ("Saved: " + args [5]); } else { LogFile.WriteLine ("Failed to upload some files!"); } } //Update Crash with Summary and Description Info bool updateSuccess = false; string logDirectory2; int endOfLogPath2 = args [2].LastIndexOf ('/'); logDirectory2 = args [2].Substring (0, endOfLogPath2 + 1); logFileName = logDirectory2 + "AutoReportLog.txt"; LogFile.WriteLine ("Attempting to add the crash description..."); try { updateSuccess = reportService.AddCrashDescription (uniqueIndex, CrashDescription, Summary); } catch (Exception e) { LogFile.WriteLine ("AutoReporter had an exception adding crash description! --> " + e.ToString ()); serviceException = e; serviceError = true; updateSuccess = false; LogFile.WriteLine (e.Message); } if (uniqueIndex != -1) { LogFile.WriteLine ("Attempting to write the crash report URL..."); #if DEBUG string strCrashURL = Properties.Settings.Default.CrashURL_Debug + uniqueIndex; #else string strCrashURL = Properties.Settings.Default.CrashURL + uniqueIndex; #endif LogFile.WriteLine ("CrashReport url = " + strCrashURL); LogFile.WriteLine (""); if (NotifyObject != null) { // Set up text to inform the user of the report submission String MsgText = "Your crash report was submitted, and the URL was copied to your clipboard. Click this balloon to display the crash report."; // setup crash report URL ready for use by the notification Clipboard.SetText (strCrashURL); m_CrashReportUrl = strCrashURL; // launch a notification that when clicked will link // to the crash report url. NotifyObject.SetEvents(OnBalloonClicked); NotifyObject.Show(MsgText, BalloonTimeInMs); } string str = reportData.PlatformName.ToLower (); if (str == "pc" || str == "mac") { LogFile.WriteLine ("Attempting to send the crash report Id to the game log file..."); // On PC, so try just writing to the log. string AppLogFileName = Path.Combine (reportData.BaseDir, "..", reportData.GameName + "Game", "Logs", reportData.GameName + ".log"); if (!File.Exists (AppLogFileName)) { // It's also possible the log name is Engine.log (default game name from BaseEngine.ini) AppLogFileName = Path.Combine (reportData.BaseDir, "..", reportData.GameName + "Game", "Logs", "Engine.log"); if (!File.Exists (AppLogFileName)) // Default to hardcoded UE4.log AppLogFileName = Path.Combine (reportData.BaseDir, "..", reportData.GameName + "Game", "Logs", "UE4.log"); } LogFile.WriteLine ("\n"); LogFile.WriteLine ("Attempting to open log file: " + AppLogFileName); try { using (StreamWriter AppLogWriter = new StreamWriter(AppLogFileName, true)) { AppLogWriter.WriteLine (""); AppLogWriter.WriteLine ("CrashReport url = " + strCrashURL); AppLogWriter.WriteLine (""); AppLogWriter.Flush (); } } catch (System.Exception e) { LogFile.WriteLine ("AutoReporter had an exception creating a stream writer! --> " + e.ToString ()); } } } if (updateSuccess) { LogFile.WriteLine ("Successfully added crash description"); } else { LogFile.WriteLine ("Service failed to add crash description!"); LogFile.Close (); return; } LogFile.WriteLine ("Closing the AutoReporter log file..."); LogFile.Close (); try { //everything was successful, so clean up dump and log files on client System.IO.File.Delete (args [1]); System.IO.File.Delete (args [3]); //todo: need to handle partial failure cases (some files didn't upload, etc) to know if we should delete the log //System.IO.File.Delete(logFileName); } catch (Exception e) { string ExcStr = "AutoReporter had an exception deleting the temp files!\n" + e.ToString (); MessageBox.Show (ExcStr, "AutoReporter Status", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); } // Make a copy of the log file w/ the CrashReport # in it... // This is to help track down the issue where empty log files are being uploaded to the crash reporter. string CopyFileName = logFileName.Replace (".txt", "_" + uniqueIndex + ".txt"); System.IO.File.Copy (logFileName, CopyFileName); if (NotifyObject != null) { // spin until the NotifyObject has closed while (NotifyObject.IsOpen) { // sleep while the notify balloon is fading System.Threading.Thread.Sleep (1000); // force check the events. Application.DoEvents (); } } } /// /// Internal static variable used to hold the url of the crash report /// once it is generated. /// static private String m_CrashReportUrl = ""; /// /// Event handler which launches the url contained in m_CrashReportURL /// when called. Designed to be called when a balloon is clicked on. /// /// Object which sent event /// event arguments static void OnBalloonClicked(object sender, EventArgs e) { // attempts to launch crash report website try { if (m_CrashReportUrl != null) { System.Diagnostics.Process.Start(m_CrashReportUrl); } } finally { } } /// /// EpicBalloon is a subtle yet interactive user notification method. /// It should be used where the user does not need to be interrupted, /// but may require confirmation of a task completing. /// class EpicBalloon { /// /// Constructor: initializes values and sets up all required /// internal events. /// public EpicBalloon() { // initialize balloon balloon = new NotifyIcon(); balloon.BalloonTipIcon = System.Windows.Forms.ToolTipIcon.Warning; // create a new icon, (balloons require tray icons to run). var iconResource = AutoReporter.Properties.Resources.BalloonTrayIcon; balloon.Icon = new System.Drawing.Icon(iconResource, iconResource.Size); // set default title and text balloon.BalloonTipTitle = "Unreal AutoReporter"; balloon.Text = ""; // set on closed to exposed events, these are expected to close the window balloon.BalloonTipClicked += new System.EventHandler(OnClosed); balloon.BalloonTipClosed += new System.EventHandler(OnClosed); // set open flag bOpen = false; } /// /// Displays the NotifyIcon, with the desired text for the desired time. /// /// The text to show in the notification /// The time the notification is shown for. NOTE: Opperating system may override this. public void Show(String Text, int TimeDisplayed) { // launch the notification balloon.BalloonTipText = Text; balloon.Visible = true; balloon.ShowBalloonTip(TimeDisplayed); // Set internal flag to show the notification is now open. bOpen = true; } /// /// Allows for custom events to be set, all functions must /// for fill the EvtHandler delegate requirements. /// /// Called when notification is clicked /// Called when notification is closed (manually or through timeout) public void SetEvents(EvtHandler OnClick = null, EvtHandler OnFinish = null) { if (OnClick != null) { balloon.BalloonTipClicked += new System.EventHandler(OnClick); } if (OnFinish != null) { balloon.BalloonTipClosed += new System.EventHandler(OnFinish); } } /// /// Used for internal event, sets a value allowing the notification to tell /// when it has been closed. /// /// Object which sent event /// event arguments void OnClosed(object sender, EventArgs e) { bOpen = false; } /// /// Read Only property: Returns TRUE when the notification is being shown. /// public bool IsOpen { get { return bOpen; } } /// /// Internal flag, true when notification is open /// bool bOpen; /// /// .Net Notification icon. /// NotifyIcon balloon; /// /// Delegate users must for fill to use notification icons events. /// /// Object which sent event /// event arguments public delegate void EvtHandler(object sender, EventArgs e); } } }