Skip to content

Fix PlcTransportAdapter: unreliable reconnect guard and leaked stream on disconnect#159

Merged
efargas merged 3 commits into
test-payload-compilationfrom
copilot/sub-pr-158
Mar 23, 2026
Merged

Fix PlcTransportAdapter: unreliable reconnect guard and leaked stream on disconnect#159
efargas merged 3 commits into
test-payload-compilationfrom
copilot/sub-pr-158

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 23, 2026

ConnectAsync had a broken TcpClient reset condition that could throw ObjectDisposedException on _client.Client and never correctly detected a "weird state". DisconnectAsync closed the stream without nulling _stream, leaving DataAvailable, ReadAsync, and WriteAsync operating on a disposed NetworkStream, and used the async/await Task.CompletedTask antipattern.

These fixes are applied to Services.Adapters.PlcTransportAdapter — the implementation registered in DI. The unreferenced Services.Plc.Adapters.PlcTransportAdapter duplicate has been removed, leaving a single source of truth.

Changes

  • ConnectAsync: Replaced the unreliable multi-part condition with an early-return on _client?.Connected == true, then unconditionally disposes the previous _stream (setting it to null) and _client before allocating a fresh TcpClient — ensuring clean state on every connection attempt.

  • DisconnectAsync: Disposes _stream and immediately sets it to null before tearing down the client. Drops the async modifier; returns Task.CompletedTask directly.

  • Removed duplicate: Deleted the unused Services.Plc.Adapters.PlcTransportAdapter class that was never registered in DI and never referenced, eliminating the split source of truth.

// Before — stream never nulled; broken reset condition; pointless async
public async Task DisconnectAsync(CancellationToken cancellationToken = default)
{
    _stream?.Close();          // closed but not nulled
    _client?.Close();
    _client = new TcpClient();
    await Task.CompletedTask;  // antipattern
}

// After
public Task DisconnectAsync(CancellationToken cancellationToken = default)
{
    _stream?.Dispose();
    _stream = null;            // guards ReadAsync/WriteAsync/DataAvailable
    _client?.Dispose();
    _client = new TcpClient();
    return Task.CompletedTask;
}

⚡ Quickly spin up Copilot coding agent tasks from anywhere on your macOS or Windows machine with Raycast.

…leanup on disconnect

Co-authored-by: efargas <9705611+efargas@users.noreply.github.com>
Agent-Logs-Url: https://github.com/efargas/S7-Tools/sessions/44d9dd1e-b8a2-45d1-91e1-0ed96cb90f91
Copilot AI changed the title [WIP] Refactor application settings and enhance service functionalities Fix PlcTransportAdapter: unreliable reconnect guard and leaked stream on disconnect Mar 23, 2026
Copilot AI requested a review from efargas March 23, 2026 15:39
@efargas efargas marked this pull request as ready for review March 23, 2026 15:46
Copilot AI review requested due to automatic review settings March 23, 2026 15:46
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 aims to harden the PLC TCP transport adapter by ensuring clean reconnect state and preventing use-after-dispose of a NetworkStream during disconnects.

Changes:

  • Simplifies ConnectAsync guard logic and forces a fresh TcpClient per connection attempt (disposing prior client/stream).
  • Updates DisconnectAsync to dispose and null _stream, dispose the client, and drop the unnecessary async/await Task.CompletedTask pattern.

Comment on lines +46 to +58
if (_client?.Connected == true)
{
return;
}

_logger.LogInformation("Connecting to PLC via Socat at {Host}:{Port}...", _host, _port);

// Dispose previous stream and client; always use a fresh TcpClient for each connection attempt
_stream?.Dispose();
_stream = null;
_client?.Dispose();
_client = new TcpClient();

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

This fix is applied to S7Tools.Services.Plc.Adapters.PlcTransportAdapter, but the DI registration currently binds IPlcTransport to S7Tools.Services.Adapters.PlcTransportAdapter (see ServiceCollectionExtensions.cs). As a result, the reconnect/stream-disposal fix may not affect the transport implementation actually used at runtime. Consider either applying the same changes to the registered adapter (recommended) or updating DI/removing the duplicate adapter to ensure there is a single source of truth.

Copilot uses AI. Check for mistakes.
@efargas
Copy link
Copy Markdown
Owner

efargas commented Mar 23, 2026

@copilot apply changes based on the comments in this thread

…move unused Plc.Adapters duplicate

Co-authored-by: efargas <9705611+efargas@users.noreply.github.com>
Agent-Logs-Url: https://github.com/efargas/S7-Tools/sessions/6b3076ee-61ea-4021-90c9-d79753c8857c
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 23, 2026

@copilot apply changes based on the comments in this thread

Fixed in commit dd8ea1f. Applied the same ConnectAsync/DisconnectAsync fixes to Services.Adapters.PlcTransportAdapter — the class registered in DI (services.TryAddTransient<IPlcTransport, Services.Adapters.PlcTransportAdapter>()). Also deleted the unreferenced Services.Plc.Adapters.PlcTransportAdapter duplicate, leaving a single source of truth.

@efargas efargas merged commit 0f2b18d into test-payload-compilation Mar 23, 2026
@efargas efargas deleted the copilot/sub-pr-158 branch March 23, 2026 16:17
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