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

DemoBytom / DemoEngine / 22160654060

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

push

coveralls.net

web-flow
Merge pull request #503 from DemoBytom/feature/small_fixes

Fixed `DisposedException` when calling `mainLoopLifetime.Cancel`

640 of 2132 relevant lines covered (30.02%)

0.67 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; }
2✔
23
    public bool IsRunning { get; private set; }
2✔
24

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

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

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

2✔
61
                FinishRunning(tcs);
62
            }
63
            catch (OperationCanceledException)
2✔
64
            {
2✔
65
                FinishRunning(tcs);
2✔
66
            }
2✔
67
            catch (Exception ex)
68
            {
2✔
69
                FinishRunning(tcs, ex);
70
            }
71
        });
2✔
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);
2✔
76
            thread.Name = "Main STA thread";
2✔
77
        }
78
        else
79
        {
80
            thread.Name = "Main thread";
81
        }
82

83
        thread.Start();
2✔
84

85
        return tcs.Task;
2✔
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;
2✔
95
            _mainLoopLifetime.Cancel();
2✔
96

97
            if (exception is null)
98
            {
99
                tcs.SetResult();
2✔
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;
2✔
114

115
        await foreach (var staAction in _channelReader
116
            .ReadAllAsync(cancellationToken)
2✔
117
            .WithCancellation(cancellationToken))
2✔
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);
2✔
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)
2✔
140
        {
141
            if (disposing)
2✔
142
            {
143
            }
144

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

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

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

163
        public override void Post(SendOrPostCallback d, object? state)
164
            => _channel.Writer.TryWrite((d, state));
4✔
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;
4✔
172
            try
173
            {
174
                var context = new SingleThreadedSynchronizationContextChannel();
4✔
175
                SetSynchronizationContext(context);
4✔
176

177
                Task task;
178
                try
179
                {
180
                    task = taskInvoker.Invoke();
4✔
181
                }
4✔
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
4✔
190
                    => context._channel.Writer.Complete(t.Exception),
4✔
191
                    TaskScheduler.Default);
4✔
192

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

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