• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DemoBytom / DemoEngine / 22501366615

27 Feb 2026 07:49PM UTC coverage: 29.406% (+0.09%) from 29.317%
22501366615

push

coveralls.net

web-flow
Async refactor - `StaThreadRequest` and `AsyncServiceScopes` (#512)

A warning was reported due to incorrectly disposing types that only implement `IAsyncDisposable`:
```
Demo.Engine Warning: 0 : AUTOFAC: A synchronous Dispose has been attempted, but the tracked object of type 'Demo.Engine.Core.Services.MainLoopService' only implements IAsyncDisposable. This will result in an inefficient blocking dispose. Consider either implementing IDisposable on 'Demo.Engine.Core.Services.MainLoopService' or disposing of the scope/container with DisposeAsync.
```

The solution was to use an `AsyncServiceScope` retreived via `_scopeFactory.CreateAsyncScope()` instead `IServiceScope` from `_scopeFactory.CreateScope()`.
This led to a bigger refactor as scopes were also created within `RenderingSurface` which in turn then required a `IAsyncDisposable` implementation. Changes in `RenderingSurface` required changes in `StaThreadRequests` and so on. Turtles all the way down 🐢🐢🐢
Currently all async methods are properly awaited and don't deadlock. But some `.ConfigureAwaits` might need to be added. This will be evaluated later.

Fixes #511

Co-authored-by: Michał Dembski <DemoBytom@users.noreply.github.com>

644 of 2190 relevant lines covered (29.41%)

0.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.94
/src/Demo.Engine.Core/Features/StaThread/StaThreadService.cs
1
// Copyright © Michał Dembski and contributors.
2
// Distributed under MIT license. See LICENSE file in the root for more information.
3

4
using System.Runtime.InteropServices;
5
using System.Threading.Channels;
6
using Demo.Engine.Core.Interfaces;
7
using Demo.Engine.Core.Interfaces.Platform;
8
using Demo.Engine.Core.Interfaces.Rendering;
9
using Microsoft.Extensions.Hosting;
10
using Microsoft.Extensions.Logging;
11

12
namespace Demo.Engine.Core.Features.StaThread;
13

14
internal sealed class StaThreadService
15
    : IStaThreadService,
16
      IDisposable
17
{
18
    private readonly ILogger<StaThreadService> _logger;
19
    private readonly IHostApplicationLifetime _hostApplicationLifetime;
20
    private readonly ChannelReader<StaThreadRequests> _channelReader;
21
    private readonly IMainLoopLifetime _mainLoopLifetime;
22
    private bool _disposedValue;
23

24
    public Task ExecutingTask { get; }
1✔
25
    public bool IsRunning { get; private set; }
1✔
26

27
    public StaThreadService(
1✔
28
        ILogger<StaThreadService> logger,
1✔
29
        IHostApplicationLifetime hostApplicationLifetime,
1✔
30
        IRenderingEngine renderingEngine,
1✔
31
        IOSMessageHandler osMessageHandler,
1✔
32
        ChannelReader<StaThreadRequests> channelReader,
1✔
33
        IMainLoopLifetime mainLoopLifetime)
1✔
34
    {
35
        _logger = logger;
1✔
36
        _hostApplicationLifetime = hostApplicationLifetime;
1✔
37
        _channelReader = channelReader;
1✔
38
        _mainLoopLifetime = mainLoopLifetime;
1✔
39
        IsRunning = true;
1✔
40
        ExecutingTask = RunSTAThread(
1✔
41
            renderingEngine,
1✔
42
            osMessageHandler);
1✔
43
    }
1✔
44

45
    private Task RunSTAThread(
46
        IRenderingEngine renderingEngine,
47
        IOSMessageHandler osMessageHandler)
48
    {
49
        var tcs = new TaskCompletionSource();
1✔
50
        var thread = new Thread(()
1✔
51
            =>
1✔
52
        {
1✔
53
            try
1✔
54
            {
1✔
55
                using var cts = CancellationTokenSource.CreateLinkedTokenSource(
1✔
56
                    _hostApplicationLifetime.ApplicationStopping,
1✔
57
                    _mainLoopLifetime.Token);
1✔
58

1✔
59
                SingleThreadedSynchronizationContextChannel.Await(async ()
1✔
60
                    => await STAThread(
1✔
61
                        renderingEngine: renderingEngine,
1✔
62
                        osMessageHandler: osMessageHandler,
1✔
63
                        cancellationToken: cts.Token));
1✔
64

1✔
65
                FinishRunning(tcs);
66
            }
67
            catch (OperationCanceledException)
1✔
68
            {
1✔
69
                FinishRunning(tcs);
1✔
70
            }
1✔
71
            catch (Exception ex)
72
            {
1✔
73
                FinishRunning(tcs, ex);
74
            }
75
        });
1✔
76
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
77
        {
78
            //Can only by set on the Windows machine. Doesn't work on Linux/MacOS
79
            thread.SetApartmentState(ApartmentState.STA);
1✔
80
            thread.Name = "Main STA thread";
1✔
81
        }
82
        else
83
        {
84
            thread.Name = "Main thread";
85
        }
86

87
        thread.Start();
1✔
88

89
        return tcs.Task;
1✔
90

91
        void FinishRunning(
92
            TaskCompletionSource tcs,
93
            Exception? exception = null)
94
        {
95
            /* This should be called BEFORE tcs.SetResult/tcs.SetException!
96
             * Otherwise _mainLoopLifetime.Cancel() gets called after the returned tcs.Task completes,
97
             * leading to dispoes exception on mainLoopLifetime, that's already disposed upstream! */
98
            IsRunning = false;
1✔
99
            _mainLoopLifetime.Cancel();
1✔
100

101
            if (exception is null)
102
            {
103
                tcs.SetResult();
1✔
104
            }
105
            else
106
            {
107
                tcs.SetException(exception);
108
            }
109
        }
110
    }
111

112
    private async Task STAThread(
113
        IRenderingEngine renderingEngine,
114
        IOSMessageHandler osMessageHandler,
115
        CancellationToken cancellationToken)
116
    {
117
        var doEventsOk = true;
1✔
118

119
        await foreach (var staAction in _channelReader
120
            .ReadAllAsync(cancellationToken)
1✔
121
            .WithCancellation(cancellationToken))
1✔
122
        {
123
            switch (staAction)
124
            {
125
                case StaThreadRequests.DoEventsOkRequest doEventsOkRequest:
126
                    doEventsOk &= await doEventsOkRequest
127
                        .InvokeAsync(renderingEngine, osMessageHandler, cancellationToken)
128
                        .ConfigureAwait(continueOnCapturedContext: true);
129
                    break;
130

131
                default:
132
                    _ = await staAction
1✔
133
                        .InvokeAsync(renderingEngine, osMessageHandler, cancellationToken)
1✔
134
                        .ConfigureAwait(continueOnCapturedContext: true);
1✔
135
                    break;
136
            }
137

138
            if (!doEventsOk || !IsRunning || cancellationToken.IsCancellationRequested)
139
            {
140
                break;
141
            }
142
        }
143
    }
144

145
    private void Dispose(bool disposing)
146
    {
147
        if (!_disposedValue)
1✔
148
        {
149
            if (disposing)
1✔
150
            {
151
            }
152

153
            _disposedValue = true;
1✔
154
        }
155
    }
1✔
156

157
    public void Dispose()
158
    {
159
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
160
        Dispose(disposing: true);
1✔
161
        GC.SuppressFinalize(this);
1✔
162
    }
1✔
163

164
    private sealed class SingleThreadedSynchronizationContextChannel
165
        : SynchronizationContext
166
    {
167
        private readonly Channel<(SendOrPostCallback d, object? state)> _channel =
2✔
168
            Channel.CreateUnbounded<(SendOrPostCallback d, object? state)>(
2✔
169
                new UnboundedChannelOptions { SingleReader = true, SingleWriter = false });
2✔
170

171
        public override void Post(SendOrPostCallback d, object? state)
172
            => _channel.Writer.TryWrite((d, state));
2✔
173

174
        public override void Send(SendOrPostCallback d, object? state)
175
            => throw new InvalidOperationException("Synchronous operations are not supported!");
×
176

177
        public static void Await(Func<Task> taskInvoker)
178
        {
179
            var originalContext = Current;
2✔
180
            try
181
            {
182
                var context = new SingleThreadedSynchronizationContextChannel();
2✔
183
                SetSynchronizationContext(context);
2✔
184

185
                Task task;
186
                try
187
                {
188
                    task = taskInvoker.Invoke();
2✔
189
                }
2✔
190
                catch (Exception ex)
×
191
                {
192
                    // If the invoker throws synchronously, complete the channel so the pump can exit.
193
                    context._channel.Writer.Complete(ex);
×
194
                    throw;
×
195
                }
196

197
                _ = task.ContinueWith(t
2✔
198
                    => context._channel.Writer.Complete(t.Exception),
2✔
199
                    TaskScheduler.Default);
2✔
200

201
                // Pump loop: block synchronously until items are available or the writer completes.
202
                while (context._channel.Reader.WaitToReadAsync().Preserve().GetAwaiter().GetResult())
2✔
203
                {
204
                    while (context._channel.Reader.TryRead(out var work))
2✔
205
                    {
206
                        work.d.Invoke(work.state);
2✔
207
                    }
2✔
208
                }
209

210
                task.GetAwaiter().GetResult();
2✔
211
            }
×
212
            finally
213
            {
214
                SetSynchronizationContext(originalContext);
2✔
215
            }
2✔
216
        }
×
217
    }
218
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc