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

DemoBytom / DemoEngine / 22160476800

18 Feb 2026 10:32PM UTC coverage: 30.019% (+0.03%) from 29.986%
22160476800

Pull #503

coveralls.net

DemoBytom
Fixed `DisposedException` when calling `mainLoopLifetime.Cancel`

The `mainLoopLifetime.Cancel()` in `StaThreadService` was called after `TaskCompletionSource.SetResult()`. That task ending was leading to `MainLoopLifetime` being already disposed before the `Cancel()` was called, causing `DisposedException` if the completion and disposal happened fast enough.
Moving the call to `mainLoopLifetime.Cancel()` before `tcs.SetResult`/`tcs.SetException` ensures this will not happen.

Fixes #501
Pull Request #503: Fixed `DisposedException` when calling `mainLoopLifetime.Cancel`

640 of 2132 relevant lines covered (30.02%)

0.33 hits per line

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

92.59
/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

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

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

22
    public Task ExecutingTask { get; }
1✔
23
    public bool IsRunning { get; private set; }
1✔
24

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

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

1✔
55
                SingleThreadedSynchronizationContextChannel.Await(async ()
1✔
56
                    => await STAThread(
1✔
57
                        renderingEngine: renderingEngine,
1✔
58
                        osMessageHandler: osMessageHandler,
1✔
59
                        cancellationToken: cts.Token));
1✔
60

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

83
        thread.Start();
1✔
84

85
        return tcs.Task;
1✔
86

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

97
            if (exception is null)
98
            {
99
                tcs.SetResult();
1✔
100
            }
101
            else
102
            {
103
                tcs.SetException(exception);
104
            }
105
        }
106
    }
107

108
    private async Task STAThread(
109
        IRenderingEngine renderingEngine,
110
        IOSMessageHandler osMessageHandler,
111
        CancellationToken cancellationToken)
112
    {
113
        var doEventsOk = true;
1✔
114

115
        await foreach (var staAction in _channelReader
116
            .ReadAllAsync(cancellationToken)
1✔
117
            .WithCancellation(cancellationToken))
1✔
118
        {
119
            switch (staAction)
120
            {
121
                case StaThreadRequests.DoEventsOkRequest doEventsOkRequest:
122
                    doEventsOk &= doEventsOkRequest.Invoke(renderingEngine, osMessageHandler);
123
                    break;
124

125
                default:
126
                    _ = staAction.Invoke(renderingEngine, osMessageHandler);
1✔
127
                    break;
128
            }
129

130
            if (!doEventsOk || !IsRunning || cancellationToken.IsCancellationRequested)
131
            {
132
                break;
133
            }
134
        }
135
    }
136

137
    private void Dispose(bool disposing)
138
    {
139
        if (!_disposedValue)
1✔
140
        {
141
            if (disposing)
1✔
142
            {
143
            }
144

145
            _disposedValue = true;
1✔
146
        }
147
    }
1✔
148

149
    public void Dispose()
150
    {
151
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
152
        Dispose(disposing: true);
1✔
153
        GC.SuppressFinalize(this);
1✔
154
    }
1✔
155

156
    private sealed class SingleThreadedSynchronizationContextChannel
157
        : SynchronizationContext
158
    {
159
        private readonly Channel<(SendOrPostCallback d, object? state)> _channel =
2✔
160
            Channel.CreateUnbounded<(SendOrPostCallback d, object? state)>(
2✔
161
                new UnboundedChannelOptions { SingleReader = true, SingleWriter = false });
2✔
162

163
        public override void Post(SendOrPostCallback d, object? state)
164
            => _channel.Writer.TryWrite((d, state));
2✔
165

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

169
        public static void Await(Func<Task> taskInvoker)
170
        {
171
            var originalContext = Current;
2✔
172
            try
173
            {
174
                var context = new SingleThreadedSynchronizationContextChannel();
2✔
175
                SetSynchronizationContext(context);
2✔
176

177
                Task task;
178
                try
179
                {
180
                    task = taskInvoker.Invoke();
2✔
181
                }
2✔
182
                catch (Exception ex)
×
183
                {
184
                    // If the invoker throws synchronously, complete the channel so the pump can exit.
185
                    context._channel.Writer.Complete(ex);
×
186
                    throw;
×
187
                }
188

189
                _ = task.ContinueWith(t
2✔
190
                    => context._channel.Writer.Complete(t.Exception),
2✔
191
                    TaskScheduler.Default);
2✔
192

193
                // Pump loop: block synchronously until items are available or the writer completes.
194
                while (context._channel.Reader.WaitToReadAsync().Preserve().GetAwaiter().GetResult())
2✔
195
                {
196
                    while (context._channel.Reader.TryRead(out var work))
2✔
197
                    {
198
                        work.d.Invoke(work.state);
2✔
199
                    }
2✔
200
                }
201

202
                task.GetAwaiter().GetResult();
2✔
203
            }
×
204
            finally
205
            {
206
                SetSynchronizationContext(originalContext);
2✔
207
            }
2✔
208
        }
×
209
    }
210
}
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