Skip to content

Commit

Permalink
Better exception presentation
Browse files Browse the repository at this point in the history
Trap and route as many exceptions as possible to the central handler.

For exceptions coming from the main thread it is possible to ignore those
and continue the app's execution, but for unhandled exceptions coming
from background threads the app will terminate.

The main focus of this work is to intercept exceptions originated from
`Executable` (i.e. executing git commands, etc.), and user scripts.
However it is not limited in this scope.

Resolves gitextensions#7795
  • Loading branch information
RussKie committed Dec 15, 2020
1 parent 919fff3 commit be0fab1
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 30 deletions.
13 changes: 11 additions & 2 deletions GitCommands/Git/Executable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,18 @@ public ProcessWrapper(string fileName, string arguments, string workDir, bool cr

_process.Exited += OnProcessExit;

_process.Start();
try
{
_process.Start();
_logOperation.SetProcessId(_process.Id);
}
catch (Exception ex)
{
Dispose();

_logOperation.SetProcessId(_process.Id);
_logOperation.LogProcessEnd(ex);
throw new ExternalOperationException(fileName, arguments, workDir, ex);
}
}

private void OnProcessExit(object sender, EventArgs eventArgs)
Expand Down
44 changes: 44 additions & 0 deletions GitCommands/Git/ExternalOperationException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;

#nullable enable

namespace GitCommands
{
/// <summary>
/// Represents errors that occur during execution of an external operation,
/// e.g. running a git operation or launching an external process.
/// </summary>
public class ExternalOperationException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="ExternalOperationException"/> class with a specified parameters
/// and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="command">The command that led to the exception.</param>
/// <param name="arguments">The command arguments.</param>
/// <param name="workingDirectory">The working directory.</param>
/// <param name="innerException">The exception that is the cause of the current exception.</param>
public ExternalOperationException(string command, string arguments, string workingDirectory, Exception innerException)
: base(null, innerException)
{
Command = command;
Arguments = arguments;
WorkingDirectory = workingDirectory;
}

/// <summary>
/// The command that led to the exception.
/// </summary>
public string Command { get; }

/// <summary>
/// The command arguments.
/// </summary>
public string Arguments { get; }

/// <summary>
/// The working directory.
/// </summary>
public string WorkingDirectory { get; }
}
}
25 changes: 25 additions & 0 deletions GitCommands/Git/UserExternalOperationException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;

#nullable enable

namespace GitCommands
{
/// <summary>
/// Represents errors that occur during execution of user-configured operation, e.g. a script.
/// </summary>
public class UserExternalOperationException : ExternalOperationException
{
/// <summary>
/// Initializes a new instance of the <see cref="UserExternalOperationException"/> class with a specified parameters
/// and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="command">The command that led to the exception.</param>
/// <param name="arguments">The command arguments.</param>
/// <param name="workingDirectory">The working directory.</param>
/// <param name="innerException">The exception that is the cause of the current exception.</param>
public UserExternalOperationException(string command, string arguments, string workingDirectory, Exception innerException)
: base(command, arguments, workingDirectory, innerException)
{
}
}
}
25 changes: 6 additions & 19 deletions GitExtensions/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ private static void Main()
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);

// if the error happens before we had a chance to init the environment information
// the call to GetInformation() will fail. A double Initialise() call is safe.
UserEnvironmentInformation.Initialise(ThisAssembly.Git.Sha, ThisAssembly.Git.IsDirty);

ThemeModule.Load();
Application.ApplicationExit += (s, e) => ThemeModule.Unload();

Expand All @@ -48,8 +52,8 @@ private static void Main()

if (!Debugger.IsAttached)
{
AppDomain.CurrentDomain.UnhandledException += (s, e) => ReportBug((Exception)e.ExceptionObject);
Application.ThreadException += (s, e) => ReportBug(e.Exception);
AppDomain.CurrentDomain.UnhandledException += (s, e) => BugReporter.Report((Exception)e.ExceptionObject, e.IsTerminating);
Application.ThreadException += (s, e) => BugReporter.Report(e.Exception, isTerminating: false);
}
}
catch (TypeInitializationException tie)
Expand Down Expand Up @@ -356,22 +360,5 @@ private static bool LocateMissingGit()
}
}
}

private static void ReportBug(Exception ex)
{
// if the error happens before we had a chance to init the environment information
// the call to GetInformation() will fail. A double Initialise() call is safe.
UserEnvironmentInformation.Initialise(ThisAssembly.Git.Sha, ThisAssembly.Git.IsDirty);
var envInfo = UserEnvironmentInformation.GetInformation();

using (var form = new GitUI.NBugReports.BugReportForm())
{
var result = form.ShowDialog(ex, envInfo);
if (result == DialogResult.Abort)
{
Environment.Exit(-1);
}
}
}
}
}
1 change: 1 addition & 0 deletions GitUI/CommandsDialogs/SettingsDialog/CheckSettingsLogic.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows.Forms;
using GitCommands;
using GitCommands.Settings;
using GitCommands.Utils;
Expand Down
4 changes: 0 additions & 4 deletions GitUI/MessageBoxes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,6 @@ internal MessageBoxes()

private static MessageBoxes Instance => instance ?? (instance = new MessageBoxes());

public static void FailedToExecuteScript(IWin32Window owner, string scriptKey, Exception ex)
=> ShowError(owner, $"{Instance._failedToExecuteScript.Text} {scriptKey.Quote()}.{Environment.NewLine}"
+ $"{Instance._reason.Text}: {ex.Message}");

public static void FailedToRunShell(IWin32Window owner, string shell, Exception ex)
=> ShowError(owner, $"{Instance._failedToRunShell.Text} {shell.Quote()}.{Environment.NewLine}"
+ $"{Instance._reason.Text}: {ex.Message}");
Expand Down
141 changes: 141 additions & 0 deletions GitUI/NBugReports/BugReporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using System;
using System.Windows.Forms;
using GitCommands;
using GitUI;
using Microsoft.WindowsAPICodePack.Dialogs;

namespace GitExtensions
{
public static class BugReporter
{
public static void Report(Exception ex, bool isTerminating)
{
if (ex is UserExternalOperationException userExternalException)
{
// Something happened that was likely caused by the user
ReportUserException(userExternalException, isTerminating);
return;
}

if (ex is ExternalOperationException externalException)
{
// Something happened either in the app - can submit the bug to GitHub
ReportAppException(externalException, isTerminating);
return;
}

// Report any other exceptions
ReportGenericException(ex, isTerminating);
}

private static void ReportAppException(ExternalOperationException ex, bool isTerminating)
{
IWin32Window owner = Application.OpenForms.Count > 0 ? Application.OpenForms[0] : null;

// UserExternalOperationException wraps an actual exception, but be cautious just in case
string message = ex.InnerException?.Message ?? Strings.InstructionOperationFailed;

using var dialog = new TaskDialog
{
OwnerWindowHandle = owner?.Handle ?? IntPtr.Zero,
Text = $"{Strings.Command}: {ex.Command.Quote()} {ex.Arguments.QuoteNE()}\r\n{Strings.WorkingDirectory}: {ex.WorkingDirectory.Quote()}\r\n\r\n{Strings.ReportBug}",
InstructionText = message,
Caption = Strings.Error,
Icon = TaskDialogStandardIcon.Error,
Cancelable = true,
};
var btnReport = new TaskDialogCommandLink("Report", Strings.ButtonReportBug);
btnReport.Click += (s, e) =>
{
dialog.Close();
ShowNBug(ex);
};
var btnIgnoreOrClose = new TaskDialogCommandLink("IgnoreOrClose", isTerminating ? Strings.ButtonCloseApp : Strings.ButtonIgnore);
btnIgnoreOrClose.Click += (s, e) =>
{
dialog.Close();
};
dialog.Controls.Add(btnReport);
dialog.Controls.Add(btnIgnoreOrClose);

dialog.Show();
}

private static void ReportUserException(UserExternalOperationException ex, bool isTerminating)
{
IWin32Window owner = Application.OpenForms.Count > 0 ? Application.OpenForms[0] : null;

// UserExternalOperationException wraps an actual exception, but be cautious just in case
string message = ex.InnerException?.Message ?? Strings.InstructionOperationFailed;

using var dialog = new TaskDialog
{
OwnerWindowHandle = owner?.Handle ?? IntPtr.Zero,
Text = $"{Strings.Command}: {ex.Command.Quote()} {ex.Arguments.QuoteNE()}\r\n{Strings.WorkingDirectory}: {ex.WorkingDirectory.Quote()}",
InstructionText = message,
Caption = Strings.CaptionFailedExecute,
Icon = TaskDialogStandardIcon.Error,
Cancelable = true,
};
var btnIgnoreOrClose = new TaskDialogCommandLink("IgnoreOrClose", isTerminating ? Strings.ButtonCloseApp : Strings.ButtonIgnore);
btnIgnoreOrClose.Click += (s, e) =>
{
dialog.Close();
};
dialog.Controls.Add(btnIgnoreOrClose);

dialog.Show();
}

private static void ReportGenericException(Exception ex, bool isTerminating)
{
IWin32Window owner = Application.OpenForms.Count > 0 ? Application.OpenForms[0] : null;

// This exception is arbitrary, see if there's additional information
string moreInfo = ex.InnerException?.Message;
if (moreInfo != null)
{
moreInfo += "\r\n\r\n";
}

using var dialog = new TaskDialog
{
OwnerWindowHandle = owner?.Handle ?? IntPtr.Zero,
Text = $"{moreInfo}{Strings.ReportBug}",
InstructionText = ex.Message,
Caption = Strings.Error,
Icon = TaskDialogStandardIcon.Error,
Cancelable = true,
};
var btnReport = new TaskDialogCommandLink("Report", Strings.ButtonReportBug);
btnReport.Click += (s, e) =>
{
dialog.Close();
ShowNBug(ex);
};
var btnIgnoreOrClose = new TaskDialogCommandLink("IgnoreOrClose", isTerminating ? Strings.ButtonCloseApp : Strings.ButtonIgnore);
btnIgnoreOrClose.Click += (s, e) =>
{
dialog.Close();
};
dialog.Controls.Add(btnReport);
dialog.Controls.Add(btnIgnoreOrClose);

dialog.Show();
}

private static void ShowNBug(Exception ex)
{
var envInfo = UserEnvironmentInformation.GetInformation();

using (var form = new GitUI.NBugReports.BugReportForm())
{
var result = form.ShowDialog(ex, envInfo);
if (result == DialogResult.Abort)
{
Environment.Exit(-1);
}
}
}
}
}
5 changes: 2 additions & 3 deletions GitUI/Script/ScriptRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,9 @@ public static CommandStatus RunScript(IWin32Window owner, IGitModule module, str
return RunScript(owner, module, scriptKey, uiCommands, revisionGrid,
msg => MessageBox.Show(owner, msg, Strings.Error, MessageBoxButtons.OK, MessageBoxIcon.Error));
}
catch (Exception ex)
catch (ExternalOperationException ex)
{
MessageBoxes.FailedToExecuteScript(owner, scriptKey, ex);
return new CommandStatus(false, false);
throw new UserExternalOperationException(ex.Command, ex.Arguments, ex.WorkingDirectory, ex.InnerException ?? ex);
}
}

Expand Down
20 changes: 20 additions & 0 deletions GitUI/Strings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ internal sealed class Strings : Translate

private readonly TranslationString _buttonCheckoutBranch = new TranslationString("Checkout branch");
private readonly TranslationString _buttonContinue = new TranslationString("Continue");
private readonly TranslationString _buttonCloseApp = new TranslationString("Close application");
private readonly TranslationString _buttonCreateBranch = new TranslationString("Create branch");
private readonly TranslationString _buttonIgnore = new TranslationString("Ignore");
private readonly TranslationString _buttonReportBug = new TranslationString("Report bug!");

private readonly TranslationString _captionFailedExecute = new TranslationString("Failed to execute");
private readonly TranslationString _instructionOperationFailed = new TranslationString("Operation failed");

private readonly TranslationString _containedInCurrentCommitText = new TranslationString("'{0}' is contained in the currently selected commit");
private readonly TranslationString _containedInBranchesText = new TranslationString("Contained in branches:");
Expand Down Expand Up @@ -86,6 +92,10 @@ internal sealed class Strings : Translate

private readonly TranslationString _rotInactive = new TranslationString("[ Inactive ]");

private readonly TranslationString _commandText = new TranslationString("Command");
private readonly TranslationString _workingDirectoryText = new TranslationString("Working directory");
private readonly TranslationString _reportBugText = new TranslationString("If you think this was caused by Git Extensions you can report a bug for the team to investigate.");

// public only because of FormTranslate
public Strings()
{
Expand All @@ -111,7 +121,13 @@ public static void Reinitialize()

public static string ButtonContinue => _instance.Value._buttonContinue.Text;
public static string ButtonCheckoutBranch => _instance.Value._buttonCheckoutBranch.Text;
public static string ButtonCloseApp => _instance.Value._buttonCloseApp.Text;
public static string ButtonCreateBranch => _instance.Value._buttonCreateBranch.Text;
public static string ButtonIgnore => _instance.Value._buttonIgnore.Text;
public static string ButtonReportBug => _instance.Value._buttonReportBug.Text;

public static string CaptionFailedExecute => _instance.Value._captionFailedExecute.Text;
public static string InstructionOperationFailed => _instance.Value._instructionOperationFailed.Text;

public static string ContainedInCurrentCommit => _instance.Value._containedInCurrentCommitText.Text;
public static string ContainedInBranches => _instance.Value._containedInBranchesText.Text;
Expand Down Expand Up @@ -177,5 +193,9 @@ public static void Reinitialize()
public static string ShowDiffForAllParentsTooltip => _instance.Value._showDiffForAllParentsTooltip.Text;

public static string Inactive => _instance.Value._rotInactive.Text;

public static string Command => _instance.Value._commandText.Text;
public static string WorkingDirectory => _instance.Value._workingDirectoryText.Text;
public static string ReportBug => _instance.Value._reportBugText.Text;
}
}
Loading

0 comments on commit be0fab1

Please sign in to comment.