Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BackgroundService.StopAsync never completes when called from a WPF project from Application.Exit event or Application.OnExit override #42993

Closed
codingdna2 opened this issue Jun 16, 2020 · 11 comments

Comments

@codingdna2
Copy link

Description

BackgroundService StopAsync never completes when called from a WPF project from Application.Exit event or Application.OnExit override. Not sure this is the right repo to open the issue so be patient with me.

To Reproduce

Steps to reproduce the behavior (sample project)

Relevant parts:

public partial class App : Application
{
    private IHost _host;

    public static IHostBuilder CreateHostBuilder() => new HostBuilder().ConfigureAppConfiguration((context, configurationBuilder) =>
    {
        configurationBuilder.AddEnvironmentVariables();
        configurationBuilder.SetBasePath(context.HostingEnvironment.ContentRootPath);
    });

    public App()
    {
        _host = CreateHostBuilder().ConfigureServices((context, services) =>
        {
            services.AddHostedService<MyService>();
            services.AddSingleton<MainWindow>();
        }).Build();
    }

    private async void OnApplicationExit(object sender, ExitEventArgs e)
    {
        await _host.StopAsync();

        _host.Dispose();

        _host = null;
    }

    private async void OnApplicationStartup(object sender, StartupEventArgs e)
    {
        await _host.StartAsync();

        var mainWindow = _host.Services.GetService<MainWindow>();
        mainWindow.Show();
    }
}
public class MyService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await Task.Delay(10, stoppingToken);
        }
    }
}

Expected behavior

The StopAsync should complete or timeout.

Additional context

I think a deadlock occurs but I've no idea how to prevent it. I've tried to debug the BackgroundService class and the offending part is the Task.WhenAny included in StopAsync. I've also tried to wrap the call to StopAsync in a Task but the effect is the same.

Any suggestion is appreciated.

@OAguinagalde
Copy link

I found myself in the same situation. My use is pretty much the same:

    public partial class App : Application
    {
        private IHost _host;
        public App()
        {
            _host = CreateHostBuilder().Build();
        }
        private async void Application_Startup(object sender, StartupEventArgs e)
        {
            await _host.StartAsync();
            _host.Services.GetService<MainWindow>().Show();
        }
        private async void Application_Exit(object sender, ExitEventArgs e)
        {
            using (_host)
            {
                _host.Services.GetService<MainWindow>().Close();
                await _host.StopAsync(TimeSpan.FromSeconds(5));
            }
        }
    }

After calling Close() on the MainWindow my method Application_Exit is called, and I get this in my logs:

Information: Microsoft.Hosting.Lifetime[0]: Waiting for the host to be disposed. Ensure all 'IHost' instances are wrapped in 'using' blocks.

Maybe there is a "proper way" of using Microsoft.Extensions and IHost with WPF but I have no idea how to make them both work properly together.

When putting the _host in a using statement in all of the methods: the constructor App() , Application_Startup and Application_Exit, then the problem doesn't happen and the application closes properly, but of course, disposing the IHost right after constructing is definetely not the way to go haha.

Any insights on how to make Microsoft.Extensions and WPF work together nicely?

@OAguinagalde
Copy link

I got some help in the C# Discord, here's a workaround:

private async void Application_Exit(object sender, ExitEventArgs e)
{
    _host.Services.GetService<MainWindow>().Close();
    var stopTask = _host.StopAsync(TimeSpan.FromSeconds(5));
    _host.Dispose();
    await stopTask;
}

For some reason, the StopAsync never actually exits. So, while not a good solution, it works. Still wondering what the proper way would be.

@codingdna2
Copy link
Author

Hi, thanks for the workaround. While the workaround works most of the times, it sometimes causes an ObjectDisposedException in my program.

@OAguinagalde
Copy link

Yeah it's not a good solution, but can't really find any help with how to approach this situation. As for the exception, a good try/catch and that would do it haha.

@BrennanConroy
Copy link
Member

This looks similar to #35990

Another workaround is to register a different IHostLifetime that doesn't block.

@codingdna2
Copy link
Author

codingdna2 commented Sep 24, 2020

I've tried to implement IHostLifetime but as the other solution, the IHostedService(s) are not shutdown gracefully. In fact, IHostLifetime.StopAsync is never called, as the IHostedService.StopAsync never completes.

EDIT: Updated sample project with all the relevant parts (IHostLifetime and IHostedService implementations)

Here's my attempt:

public class WPFAppLifetime : IHostLifetime, IDisposable
{
    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }

    public Task WaitForStartAsync(CancellationToken cancellationToken)
    {
        AppDomain.CurrentDomain.ProcessExit += OnProcessExit;

        return Task.CompletedTask;
    }

    private void OnProcessExit(object sender, EventArgs e)
    {
        Environment.ExitCode = 0;
    }

    public void Dispose()
    {
        AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;
    }
}
public static class WPFLifetimeHostExtensions
{
    public static IHostBuilder UseWPFAppLifetime(this IHostBuilder hostBuilder)
    {
        return hostBuilder.ConfigureServices((hostContext, services) => services.AddSingleton<IHostLifetime, WPFAppLifetime>());
    }
}
_host = CreateHostBuilder().ConfigureServices((context, services) =>
{
    services.AddHostedService<MyService>();
    services.AddSingleton<MainWindow>();
}).UseWPFAppLifetime().Build();

@BrennanConroy BrennanConroy transferred this issue from dotnet/extensions Oct 2, 2020
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-Extensions-Hosting untriaged New issue has not been triaged by the area owner labels Oct 2, 2020
@ghost
Copy link

ghost commented Oct 2, 2020

Tagging subscribers to this area: @eerhardt, @maryamariyan
See info in area-owners.md if you want to be subscribed.

@maryamariyan maryamariyan added bug and removed untriaged New issue has not been triaged by the area owner labels Oct 8, 2020
@maryamariyan maryamariyan added this to Uncommitted in ML, Extensions, Globalization, etc, POD. via automation Oct 8, 2020
@maryamariyan maryamariyan added this to the 6.0.0 milestone Oct 8, 2020
@mayorovp
Copy link
Contributor

mayorovp commented Oct 9, 2020

Try this:

    private void OnApplicationExit(object sender, ExitEventArgs e) // this method cannot be async, it MUST wait!
    {
        Task.Run(() => _host.StopAsync()).Wait();

        _host.Dispose();

        _host = null;
    }

Or something like this:

    private void OnApplicationExit(object sender, ExitEventArgs e) // this method cannot be async, it MUST wait!
    {
        var frame = new DispatcherFrame(false);
        
        Task.Run(() => {
            try {
                 await _host.StopAsync();
            }
            finally {
                 frame.Continue = false;
            }
        });

        Dispatcher.PushFrame(frame);

        _host.Dispose();

        _host = null;
    }

@codingdna2
Copy link
Author

codingdna2 commented Oct 12, 2020

Test results, suggested solution 1:
Changed slightly adding async: Task.Run(async () => await _host.StopAsync()).Wait();
but this one ends with an AggregateException 'Remove root: handle=10, parent=01s.WpfTap.Utility.)' containing a TaskCanceledException which (I think) referes to the actual task.

Test results, suggested solution 2:
Changed slightly adding async. This one appears to be working as expected! Thanks!
Updated sample repo with this fix.

I leave to owners decision to close issue or not.

codingdna2 added a commit to codingdna2/WPFWithBackgroundService that referenced this issue Oct 12, 2020
@eerhardt
Copy link
Member

With #56057 we no longer block on ProcessExit in the ConsoleLifetime on net6.0. So this issue is now fixed.

However, the correct fix here is to not use ConsoleLifetime in a WPF app, since it is not a "console app". So implementing your own WpfLifetime is the right idea.

ML, Extensions, Globalization, etc, POD. automation moved this from 6.0.0 to Done Jul 22, 2021
@codingdna2
Copy link
Author

codingdna2 commented Jul 22, 2021

Thanks @eerhardt, but I should clarify the issue here was not the ConsoleLifetime (in fact implementing WpfAppLifetime was a test I've commented above) but the usage of Tasks from the OnApplicationExit event.

@ghost ghost locked as resolved and limited conversation to collaborators Aug 22, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

7 participants