Skip to content

Commit

Permalink
Handle domain-level exceptions more gracefully
Browse files Browse the repository at this point in the history
Closes #42
  • Loading branch information
Tyrrrz committed Dec 25, 2023
1 parent f307d99 commit 94f0d3b
Show file tree
Hide file tree
Showing 16 changed files with 174 additions and 107 deletions.
13 changes: 2 additions & 11 deletions DotnetRuntimeBootstrapper.AppHost.Cli/Bootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,8 @@ public class Bootstrapper : BootstrapperBase
{
protected override void ReportError(string message)
{
base.ReportError(message);

try
{
using (ConsoleEx.WithForegroundColor(ConsoleColor.DarkRed))
Console.Error.WriteLine("ERROR: " + message);
}
catch
{
// Ignore
}
using (ConsoleEx.WithForegroundColor(ConsoleColor.DarkRed))
Console.Error.WriteLine("ERROR: " + message);
}

protected override bool Prompt(
Expand Down
78 changes: 56 additions & 22 deletions DotnetRuntimeBootstrapper.AppHost.Core/BootstrapperBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,63 @@ public abstract class BootstrapperBase
protected BootstrapperConfiguration Configuration { get; } =
BootstrapperConfiguration.Resolve();

protected virtual void ReportError(string message)
protected abstract void ReportError(string message);

private void HandleException(Exception exception)
{
// Report to the Windows Event Log. Adapted from:
// https://github.com/dotnet/runtime/blob/57bfe474518ab5b7cfe6bf7424a79ce3af9d6657/src/native/corehost/apphost/apphost.windows.cpp#L37-L51
try
// For domain-level exceptions, report only the message
if (exception is BootstrapperException bootstrapperException)
{
var applicationFilePath = Assembly.GetExecutingAssembly().Location;
var applicationName = Path.GetFileName(applicationFilePath);
var bootstrapperVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(3);

var content = $"""
Description: Bootstrapper for a .NET application has failed.
Application: {applicationName}
Path: {applicationFilePath}
AppHost: .NET Runtime Bootstrapper v{bootstrapperVersion}
Message: {message}
""";

EventLog.WriteEntry(".NET Runtime", content, EventLogEntryType.Error, 1023);
try
{
ReportError(bootstrapperException.Message);
}
catch
{
// Ignore
}
}
catch
// For other (unexpected) exceptions, report the full stack trace and
// record the error to the Windows Event Log.
else
{
// Ignore
try
{
ReportError(exception.ToString());
}
catch
{
// Ignore
}

// Report to the Windows Event Log. Adapted from:
// https://github.com/dotnet/runtime/blob/57bfe474518ab5b7cfe6bf7424a79ce3af9d6657/src/native/corehost/apphost/apphost.windows.cpp#L37-L51
try
{
var applicationFilePath = Assembly.GetExecutingAssembly().Location;
var applicationName = Path.GetFileName(applicationFilePath);

var bootstrapperVersion = Assembly
.GetExecutingAssembly()
.GetName()
.Version
.ToString(3);

var content = $"""
Description: Bootstrapper for a .NET application has failed.
Application: {applicationName}
Path: {applicationFilePath}
AppHost: .NET Runtime Bootstrapper v{bootstrapperVersion}

{exception}
""";

EventLog.WriteEntry(".NET Runtime", content, EventLogEntryType.Error, 1023);
}
catch
{
// Ignore
}
}
}

Expand Down Expand Up @@ -85,7 +119,7 @@ private int Run(TargetAssembly targetAssembly, string[] args)
}
// Possible exception causes:
// - .NET host not found (DirectoryNotFoundException)
// - .NET host failed to initialize (ApplicationException)
// - .NET host failed to initialize (BootstrapperException)
catch
{
// Check for missing prerequisites and install them
Expand Down Expand Up @@ -115,7 +149,7 @@ private int Run(TargetAssembly targetAssembly, string[] args)
public int Run(string[] args)
{
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
ReportError(e.ExceptionObject.ToString());
HandleException((Exception)e.ExceptionObject);

try
{
Expand All @@ -131,7 +165,7 @@ public int Run(string[] args)
}
catch (Exception ex)
{
ReportError(ex.ToString());
HandleException(ex);
return 0xDEAD;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;

namespace DotnetRuntimeBootstrapper.AppHost.Core;

public class BootstrapperException : Exception
{
public BootstrapperException(string message, Exception? innerException = null)
: base(message, innerException) { }
}
25 changes: 15 additions & 10 deletions DotnetRuntimeBootstrapper.AppHost.Core/Dotnet/DotnetHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,14 @@ private nint Initialize(string targetFilePath, string[] args)
var error =
errorBuffer.Length > 0 ? errorBuffer.ToString() : "No error messages reported.";

throw new ApplicationException(
throw new BootstrapperException(
$"""
Failed to initialize .NET host.
- Target: {targetFilePath}
- Arguments: [{string.Join(", ", args)}]
- Status: {status}
- Error: {error}
"""
Failed to initialize .NET host.
- Target: {targetFilePath}
- Arguments: [{string.Join(", ", args)}]
- Status: {status}
- Error: {error}
"""
);
}

Expand All @@ -95,7 +95,7 @@ private int Run(nint handle)
// This is thrown when the app crashes with an unhandled exception.
// Unfortunately, there is no way to get the original exception or its message.
// https://github.com/Tyrrrz/DotnetRuntimeBootstrapper/issues/23
throw new ApplicationException(
throw new BootstrapperException(
"Application crashed with an unhandled exception. "
+ "Unfortunately, it was not possible to retrieve the exception message or its stacktrace. "
+ "Please check the Windows Event Viewer to see if the runtime logged any additional information. "
Expand Down Expand Up @@ -145,10 +145,13 @@ private static string GetHostResolverFilePath()
"host",
"fxr"
);

if (!Directory.Exists(hostResolverRootDirPath))
{
throw new DirectoryNotFoundException(
"Could not find directory containing hostfxr.dll."
"Failed to locate the host resolver directory ('host/fxr')."
);
}

var hostResolverFilePath = (
from dirPath in Directory.GetDirectories(hostResolverRootDirPath)
Expand All @@ -161,7 +164,9 @@ select filePath
).FirstOrDefault();

return hostResolverFilePath
?? throw new FileNotFoundException("Could not find hostfxr.dll.");
?? throw new FileNotFoundException(
"Failed to locate the host resolver file ('hostfxr.dll')."
);
}

public static DotnetHost Load() => new(NativeLibrary.Load(GetHostResolverFilePath()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,7 @@ internal static class DotnetInstallation
??
// Try to resolve location from program files (default location)
TryGetDirectoryPathFromEnvironment()
?? throw new DirectoryNotFoundException("Could not find .NET installation directory.");
?? throw new DirectoryNotFoundException(
"Failed to locate the .NET installation directory."
);
}
17 changes: 10 additions & 7 deletions DotnetRuntimeBootstrapper.AppHost.Core/Dotnet/DotnetRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ internal partial class DotnetRuntime
public static DotnetRuntime[] GetAllInstalled()
{
var sharedDirPath = Path.Combine(DotnetInstallation.GetDirectoryPath(), "shared");

if (!Directory.Exists(sharedDirPath))
{
throw new DirectoryNotFoundException(
"Could not find directory containing .NET runtime binaries."
"Failed to find the directory containing .NET runtime binaries."
);
}

return (
from runtimeDirPath in Directory.GetDirectories(sharedDirPath)
Expand All @@ -57,15 +60,15 @@ static DotnetRuntime ParseRuntime(JsonNode json)

return !string.IsNullOrEmpty(name) && version is not null
? new DotnetRuntime(name, version)
: throw new ApplicationException(
"Could not parse runtime info from runtime config."
: throw new InvalidOperationException(
"Failed to extract runtime information from the provided runtime configuration."
);
}

var json =
Json.TryParse(File.ReadAllText(runtimeConfigFilePath))
?? throw new ApplicationException(
$"Failed to parse runtime config '{runtimeConfigFilePath}'."
?? throw new InvalidOperationException(
$"Failed to parse runtime configuration at '{runtimeConfigFilePath}'."
);

return
Expand All @@ -82,8 +85,8 @@ static DotnetRuntime ParseRuntime(JsonNode json)
?.ToSingletonEnumerable()
.Select(ParseRuntime)
.ToArray()
?? throw new ApplicationException(
"Could not resolve target runtime from runtime config."
?? throw new InvalidOperationException(
"Failed to resolve the target runtime from runtime configuration."
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ runtime switch
)
.Select(f => f.TryGetChild("url")?.TryGetString())
.FirstOrDefault()
?? throw new ApplicationException(
"Failed to resolve download URL for the required .NET runtime. "
?? throw new InvalidOperationException(
"Failed to resolve the download URL for the required .NET runtime. "
+ $"Please try to download ${DisplayName} manually "
+ $"from https://dotnet.microsoft.com/download/dotnet/{runtime.Version.ToString(2)} or "
+ "from https://get.dot.net."
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System.ComponentModel;
using DotnetRuntimeBootstrapper.AppHost.Core.Utils;

namespace DotnetRuntimeBootstrapper.AppHost.Core.Prerequisites;
Expand All @@ -12,25 +12,38 @@ internal class ExecutablePrerequisiteInstaller(IPrerequisite prerequisite, strin

public PrerequisiteInstallerResult Run()
{
var exitCode = CommandLine.Run(
FilePath,
new[] { "/install", "/quiet", "/norestart" },
true
);
try
{
var exitCode = CommandLine.Run(
FilePath,
new[] { "/install", "/quiet", "/norestart" },
true
);

// https://github.com/Tyrrrz/DotnetRuntimeBootstrapper/issues/24#issuecomment-1021447102
if (exitCode is 3010 or 3011 or 1641)
return PrerequisiteInstallerResult.RebootRequired;

// https://github.com/Tyrrrz/DotnetRuntimeBootstrapper/issues/24#issuecomment-1021447102
if (exitCode is 3010 or 3011 or 1641)
return PrerequisiteInstallerResult.RebootRequired;
if (exitCode != 0)
{
throw new BootstrapperException(
$"Failed to install '{Prerequisite.DisplayName}'. "
+ $"Exit code: {exitCode}. "
+ $"Restart the application to try again, or install this component manually."
);
}

if (exitCode != 0)
return PrerequisiteInstallerResult.Success;
}
// Installation was canceled before the process could start
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
{
throw new ApplicationException(
$"Failed to install {Prerequisite.DisplayName}. "
+ $"Exit code: {exitCode}. "
+ "Please try to install this component manually."
throw new BootstrapperException(
$"Failed to install '{Prerequisite.DisplayName}'. "
+ $"The operation was canceled. "
+ $"Restart the application to try again, or install this component manually.",
ex
);
}

return PrerequisiteInstallerResult.Success;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ private string GetInstallerDownloadUrl()
return "https://download.microsoft.com/download/E/4/6/E4694323-8290-4A08-82DB-81F2EB9452C2/Windows8.1-KB2999226-x86.msu";
}

throw new ApplicationException("Unsupported operating system version.");
throw new InvalidOperationException("Unsupported operating system version.");
}

public IPrerequisiteInstaller DownloadInstaller(Action<double>? handleProgress)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ private string GetInstallerDownloadUrl()
return "https://download.microsoft.com/download/C/9/6/C96CD606-3E05-4E1C-B201-51211AE80B1E/Windows6.1-KB3063858-x86.msu";
}

throw new ApplicationException("Unsupported operating system version.");
throw new InvalidOperationException("Unsupported operating system version.");
}

public IPrerequisiteInstaller DownloadInstaller(Action<double>? handleProgress)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System.ComponentModel;
using DotnetRuntimeBootstrapper.AppHost.Core.Utils;

namespace DotnetRuntimeBootstrapper.AppHost.Core.Prerequisites;
Expand All @@ -12,21 +12,38 @@ internal class WindowsUpdatePrerequisiteInstaller(IPrerequisite prerequisite, st

public PrerequisiteInstallerResult Run()
{
var exitCode = CommandLine.Run("wusa", new[] { FilePath, "/quiet", "/norestart" }, true);
try
{
var exitCode = CommandLine.Run(
"wusa",
new[] { FilePath, "/quiet", "/norestart" },
true
);

// https://github.com/Tyrrrz/DotnetRuntimeBootstrapper/issues/24#issuecomment-1021447102
if (exitCode is 3010 or 3011 or 1641)
return PrerequisiteInstallerResult.RebootRequired;

// https://github.com/Tyrrrz/DotnetRuntimeBootstrapper/issues/24#issuecomment-1021447102
if (exitCode is 3010 or 3011 or 1641)
return PrerequisiteInstallerResult.RebootRequired;
if (exitCode != 0)
{
throw new BootstrapperException(
$"Failed to install '{Prerequisite.DisplayName}'. "
+ $"Exit code: {exitCode}. "
+ $"Restart the application to try again, or install this component manually."
);
}

if (exitCode != 0)
return PrerequisiteInstallerResult.Success;
}
// Installation was canceled before the process could start
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
{
throw new ApplicationException(
$"Failed to install {Prerequisite.DisplayName}. "
+ $"Exit code: {exitCode}. "
+ "Please try to install this component manually."
throw new BootstrapperException(
$"Failed to install '{Prerequisite.DisplayName}'. "
+ $"The operation was canceled. "
+ $"Restart the application to try again, or install this component manually.",
ex
);
}

return PrerequisiteInstallerResult.Success;
}
}
Loading

0 comments on commit 94f0d3b

Please sign in to comment.