Skip to content

Refactor UdpSourceBase to use proper Dispose pattern#7

Merged
codegefluester merged 6 commits intofeat/udp-base-sourcefrom
copilot/sub-pr-4-another-one
Feb 10, 2026
Merged

Refactor UdpSourceBase to use proper Dispose pattern#7
codegefluester merged 6 commits intofeat/udp-base-sourcefrom
copilot/sub-pr-4-another-one

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 10, 2026

Addresses feedback to replace manual disposal in finally block with proper IDisposable pattern.

Changes

  • Moved resource cleanup to Dispose(): Removed _listener.Dispose() from ReadContinuousAsync finally block, now properly overrides Dispose() method
  • Added thread-safety: Lock-based synchronization prevents race conditions during concurrent disposal
  • Prevented double disposal: Disposed flag checked atomically before cleanup
  • Exception safety: Try-finally ensures base.Dispose() executes even if listener disposal throws

Before

public override async IAsyncEnumerable<T> ReadContinuousAsync(...)
{
    try { /* ... */ }
    finally
    {
        _isListening = false;
        _listener.Dispose();  // Manual disposal in finally
    }
}

After

public override void Dispose()
{
    lock (_disposeLock)
    {
        if (!_disposed)
        {
            _disposed = true;
            try { _listener?.Dispose(); }
            finally { base.Dispose(); }
        }
    }
}

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 4 commits February 10, 2026 22:45
…sal in finally block

Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>
Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>
Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>
Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>
@codegefluester codegefluester marked this pull request as ready for review February 10, 2026 22:47
Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix feedback on adding source for F1 games Refactor UdpSourceBase to use proper Dispose pattern Feb 10, 2026
Copilot AI requested a review from codegefluester February 10, 2026 22:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR attempts to improve the disposal pattern in UdpSourceBase<T> by moving _listener.Dispose() from the finally block in ReadContinuousAsync to an overridden Dispose() method. The changes add thread-safety mechanisms including a dispose lock and disposed flag to prevent double disposal.

Changes:

  • Moved _listener.Dispose() from the finally block in ReadContinuousAsync to a new Dispose() override
  • Added thread-safety with _disposeLock and _disposed flag
  • Implemented try-finally in Dispose() to ensure base.Dispose() is always called

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 31 to +50
public override async IAsyncEnumerable<T> ReadContinuousAsync([EnumeratorCancellation] CancellationToken ct = default)
{
_isListening = true;
try
{
while (!ct.IsCancellationRequested)
{
var result = await _listener.ReceiveAsync(ct);
var data = result.Buffer;
// Process the received data and yield telemetry objects
foreach (var item in ProcessData(data))
{
yield return item;
}
}
}
finally
{
_isListening = false;
_listener.Dispose();
}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical race condition: The removal of _listener.Dispose() from the finally block creates a scenario where the UdpClient could remain undisposed if ReadContinuousAsync completes naturally (via cancellation) but Dispose() is never called. This is a resource leak.

Additionally, there's a dangerous race condition where Dispose() can be called while _listener.ReceiveAsync(ct) on line 38 is in progress. When _listener.Dispose() executes on line 62 while a receive operation is active, it can cause unpredictable behavior including ObjectDisposedException being thrown into the async enumerable consumer.

The codebase pattern (see MemoryMappedFileSource.cs:38-42, TrackmaniaMemoryMappedFileSource.cs:125-131, and FileWatcherSourceBase.cs:205-212) is to dispose resources in the finally block of ReadContinuousAsync, not in a separate Dispose() method. This ensures proper cleanup regardless of how the async enumerable terminates.

Recommendation: Keep _listener.Dispose() in the finally block where it was originally. The override Dispose() method should call base.Dispose() only, and can optionally set a flag to prevent re-initialization if needed.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +13
private readonly object _disposeLock = new object();
private bool _disposed;
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _disposed flag check doesn't prevent ReadContinuousAsync from using a disposed _listener. If Dispose() is called while ReadContinuousAsync is running, the _disposed flag will be set to true, but the running async method will continue to attempt to use _listener on line 38, which has just been disposed on line 62.

There's no check of the _disposed flag before or during ReadContinuousAsync execution. Consider adding a check at the start of ReadContinuousAsync to throw ObjectDisposedException if already disposed, and potentially in the while loop to exit gracefully if disposal occurs during iteration.

Copilot uses AI. Check for mistakes.

public override void Dispose()
{
lock (_disposeLock)
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _disposeLock is used to synchronize Dispose() calls, but it doesn't protect against concurrent access between Dispose() and ReadContinuousAsync. The lock only prevents multiple Dispose() calls from running simultaneously, but doesn't prevent Dispose() from running while ReadContinuousAsync is accessing _listener on line 38.

If the intent is to allow Dispose() to be called from a different thread while ReadContinuousAsync is running (which is the typical pattern), then the _listener resource should not be disposed in the Dispose() method at all - it should only be disposed in the finally block of ReadContinuousAsync. The Dispose() method could instead set a cancellation flag that ReadContinuousAsync can check.

Copilot uses AI. Check for mistakes.
_disposed = true;
try
{
_listener?.Dispose();
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The null-conditional operator is unnecessary here because _listener is initialized in the constructor (line 28) and is never set to null. If this is meant to handle scenarios where the constructor throws before initialization, note that partial construction is not a concern since an exception in the constructor would prevent the object from being created and Dispose wouldn't be called.

However, if the recommendation from the first comment is followed (to move disposal back to the finally block), then this line should be removed entirely from the Dispose() method.

Suggested change
_listener?.Dispose();
_listener.Dispose();

Copilot uses AI. Check for mistakes.
Comment on lines +53 to 70
public override void Dispose()
{
lock (_disposeLock)
{
if (!_disposed)
{
_disposed = true;
try
{
_listener?.Dispose();
}
finally
{
base.Dispose();
}
}
}
}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential double-disposal issue: With the current implementation, if ReadContinuousAsync completes normally and then Dispose() is called, _listener will be disposed twice - once in the new Dispose() override (line 62) and potentially again if the pattern in the documentation is followed where finally blocks also dispose resources.

Additionally, this conflicts with the planned API refactoring documented in docs/telemetry-source-api-refactoring.md (lines 97-153), which shows that _listener.Dispose() should remain in the finally block even after the refactoring. The current change makes that future refactoring more complex.

UdpClient.Dispose() is idempotent (can be called multiple times safely), but this pattern is still confusing and unnecessary.

Copilot uses AI. Check for mistakes.
@codegefluester codegefluester merged commit f238435 into feat/udp-base-source Feb 10, 2026
6 checks passed
@codegefluester codegefluester deleted the copilot/sub-pr-4-another-one branch February 10, 2026 22:53
codegefluester added a commit that referenced this pull request Feb 11, 2026
* Added F1 2025 packets

* Added F1 2024 types

* Added F1 2023 structs

* Added F1 packets and fixtures

* Added F1 2025 packets

* Added F1 2024 types

* Added F1 2023 structs

* Added F1 packets and fixtures

* Remove legacy F1 implementation workflow file

* Remove debug tooling for F1

* Add F1 fixture files to test project

* Add refactoring plan for telemetry source interface

* Remove debug logging

* Added test cases for F1 packet parsing (super basic)

* Added tests for event details parsing in F1 2025

* Tested F1 recording and reading of session recording

* Update GamesDat.Demo.Wpf/Resources/Styles.xaml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Björn Kaiser <masterchiefs2002@hotmail.com>

* Update GamesDat.Demo.Wpf/ViewModels/IRealtimeSource.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Björn Kaiser <masterchiefs2002@hotmail.com>

* [WIP] WIP Address feedback on adding source for F1 games (#5)

* Initial plan

* Fix: Change m_zoneFlag from byte to sbyte in F12025 MarshalZone

Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>

* Apply BufferSize to UDP socket ReceiveBufferSize (#6)

* Initial plan

* Apply BufferSize to socket's ReceiveBufferSize property

Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>

* Refactor UdpSourceBase to use proper Dispose pattern (#7)

* Initial plan

* Refactor UdpSourceBase to use Dispose pattern instead of manual disposal in finally block

Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>

* Add thread-safety to Dispose method in UdpSourceBase

Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>

* Move base.Dispose() inside lock for better thread-safety

Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>

* Set disposed flag before disposal operations for better exception safety

Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>

* Add try-finally to ensure base.Dispose() is always called

Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>

* Make LiveryColour struct fields public for deserialization access (#8)

* Initial plan

* Make LiveryColour fields public to allow access

Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>

* Remove unused BytesToStruct method from F1RealtimeTelemetrySource (#9)

* Initial plan

* Remove unused BytesToStruct method from F1RealtimeTelemetrySource

Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>

* [WIP] WIP address feedback from review on adding source for F1 games (#10)

* Initial plan

* Remove .claude/settings.local.json files from repository

* Push changes - removed .claude/settings.local.json files

Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>

* Remove .claude/settings.local.json files and update .gitignore pattern

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>

* Fix unsafe fixed buffer pointer handling in F1 telemetry extensions (#11)

* Initial plan

* Fix unsafe fixed buffer pointer handling and UTF-8 marshaling

- Fix F1TelemetryFrameExtensions to use Unsafe.Read<T> and direct pointer access for fixed buffers
- Change F12025 Participant m_name from ByValTStr to byte array for proper UTF-8 marshaling
- UdpSourceBase Dispose() implementation already correct (from previous PR)

Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>

* Add Name property helper for UTF-8 decoded participant name

Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>

* Fix AccessViolationException in GetPacket by using Unsafe.AsPointer

Use Unsafe.AsPointer(ref frame.RawData[0]) instead of Unsafe.Read to properly get a pointer to the fixed buffer. This resolves both the CS0213 compilation error and the runtime AccessViolationException.

Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>

* Fix frame extension and test

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>
Co-authored-by: Björn Kaiser <bjoern@codegefluester.de>

---------

Signed-off-by: Björn Kaiser <masterchiefs2002@hotmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants