lonix
lonix

Reputation: 20709

Return non-zero exit code for crashed .NET Core BackgroundService

Due to bugs in BackgroundService I'm using Stephen Cleary's excellent workaround. It works, but I can't return a non-zero exit code upon failure.

A minimal working example (for use with the $ dotnet new worker template):

MyBackgroundService.cs

public class MyBackgroundService : BackgroundService {

  private readonly IHostApplicationLifetime _lifetime;
  public MyBackgroundService(IHostApplicationLifetime lifetime) => _lifetime = lifetime;

  protected override Task ExecuteAsync(CancellationToken stoppingToken) => Task.Run(async () => {
    try {
      while (!stoppingToken.IsCancellationRequested) {
        await Task.Delay(1000);
        if (Random.Shared.NextDouble() < 0.5) throw new Exception("RANDOM CRASH!");
      }
    }
    catch {
      throw;         // I can hit a debug breakpoint here
    }
    finally {
      _lifetime.StopApplication();
    }
  });

}

Program.cs

public class Program {
  public static int Main(string[] args) {
    try {
      var builder = Host.CreateApplicationBuilder(args);
      builder.Services.AddHostedService<MyBackgroundService>();
      var host = builder.Build();
      host.Run();
      return 0;
    }
    catch (Exception) {
      return 1;                // <----- execution never arrives here
    }
  }
}

When the app crashes, I expect the exit code to be 1, but it is actually the default of 0. When I debug it never enters the catch block.

What could be the reason?

Upvotes: 1

Views: 1216

Answers (2)

lonix
lonix

Reputation: 20709

The workarounds in the accepted answer are good. But I chose the lazy route:

MyBackgroundService.cs

public class MyBackgroundService : BackgroundService {

  private readonly IHostApplicationLifetime _lifetime;
  public MyBackgroundService(IHostApplicationLifetime lifetime) => _lifetime = lifetime;

  protected override Task ExecuteAsync(CancellationToken stoppingToken) => Task.Run(async () => {
    var exitCode = 0;                                  // <---------
    try {
      while (!stoppingToken.IsCancellationRequested) {
        await Task.Delay(1000);
        if (Random.Shared.NextDouble() < 0.5) throw new Exception("RANDOM CRASH!");
      }
    }
    catch {
      exitCode = 1;                                    // <---------
      throw;
    }
    finally {
      if (Environment.ExitCode == 0 && exitCode != 0)  // <---------
        Environment.ExitCode = exitCode;               // <---------
      _lifetime.StopApplication();
    }
  });

}

Program.cs

// ...
host.Run();
return Environment.ExitCode;                           // <---------

Upvotes: 1

Stephen Cleary
Stephen Cleary

Reputation: 456657

It's actually because the task returned from BackgroundService.ExecuteAsync is ignored (as explained on my blog). So, even though you're shutting down the host, the exception is still ignored.

Hosted services don't have a way to report exceptions or exit codes. Your options are:

  • Define your own BackgroundService-derived type that exposes its Task and have your main loop resolve instances of your derived type and await those tasks. This will observe exceptions from the background services.
  • Have your background services set Environment.ExitCode if they fail, similar to this code on my blog. Then your Main can return void.

Both of these approaches do an "end run" around the app lifetime because the .NET host wasn't designed to allow background service results. I use the second one myself since the same code works for both worker processes and SCM services.

Upvotes: 3

Related Questions