Skip to content

testserver: unique notification handles + survive bad input - #539

Merged
stlehmann merged 3 commits into
stlehmann:masterfrom
yannS2016:fix/testserver-robustness
Jul 2, 2026
Merged

testserver: unique notification handles + survive bad input#539
stlehmann merged 3 commits into
stlehmann:masterfrom
yannS2016:fix/testserver-robustness

Conversation

@yannS2016

@yannS2016 yannS2016 commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

testserver: unique notification handles + survive bad input

Three small robustness fixes to the AdvancedHandler test server, found while using it to exercise an EPICS ADS driver's bulk-read and notification paths against the full symbol set of a real PLC project.

1. Unique notification handles across variables

PLCVariable.register_notification handed out colliding handles. notification_count is a class attribute, but self.notification_count += 1 rebinds it per-instance, so the first notification registered on every variable returns the same handle (10), the second 11, and so on. A client subscribed to more than one variable then cannot tell the notifications apart. Increment the shared class counter so handles are globally unique, as real TwinCAT returns them.

2. Survive an abrupt client disconnect

AdsClientConnection.run called recvfrom() with no guard. A client killed mid-request drops the connection abruptly, raising ConnectionResetError and crashing the handler thread, which leaves the server unable to serve the next connection. Treat the reset as a disconnect.

3. Survive a handler exception

A command handler that raised killed the connection's handler thread, leaving the client's later requests unanswered until they time out. handle_read_write does write_data.decode() on the read-write payload, which raises UnicodeDecodeError when the payload is not a UTF-8 symbol name (e.g. a binary SUMUP batch), and get_variable_by_name raises on an unknown symbol. Catch handler exceptions at the dispatch, log them, and return an ADS device error.

Testing

  • tests/test_testserver.py passes (3/3).
  • Added-behaviour checks: notification handles are unique across variables; a
    read-write carrying a non-UTF-8 name now returns ADS error 0x0700 instead of killing the connection thread.

Three robustness fixes to the AdvancedHandler test server, found while
using it to exercise an EPICS ADS driver's bulk-read and notification
paths against the full symbol set of a real PLC project.

1. PLCVariable.register_notification handed out colliding handles.
   notification_count is a class attribute, but `self.notification_count
   += 1` rebinds it per-instance, so the first notification registered on
   every variable returns the same handle (10), the second 11, and so on.
   A client subscribed to more than one variable then cannot tell the
   notifications apart. Increment the shared class counter so handles are
   globally unique, as real TwinCAT returns them.

2. AdsClientConnection.run called recvfrom() with no guard. A client
   killed mid-request drops the connection abruptly, raising
   ConnectionResetError and crashing the handler thread, which leaves the
   server unable to serve the next connection. Treat the reset as a
   disconnect.

3. A command handler that raised killed the connection's handler thread,
   leaving the client's later requests unanswered until they time out.
   handle_read_write does write_data.decode() on the read-write payload,
   which raises UnicodeDecodeError when the payload is not a UTF-8 symbol
   name (e.g. a binary SUMUP batch), and get_variable_by_name raises on an
   unknown symbol. Catch handler exceptions at the dispatch, log them, and
   return an ADS device error instead.
@coveralls

coveralls commented Jun 23, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 28048453040

Coverage increased (+0.3%) to 93.866%

Details

  • Coverage increased (+0.3%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 24 coverage regressions across 4 files.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

24 previously-covered lines in 4 files lost coverage.

File Lines Losing Coverage Coverage
/opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/site-packages/pyads/testserver/advanced_handler.py 9 95.13%
/opt/hostedtoolcache/Python/3.9.25/x64/lib/python3.9/site-packages/pyads/testserver/advanced_handler.py 9 95.13%
/opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/site-packages/pyads/testserver/testserver.py 3 97.58%
/opt/hostedtoolcache/Python/3.9.25/x64/lib/python3.9/site-packages/pyads/testserver/testserver.py 3 97.58%

Coverage Stats

Coverage Status
Relevant Lines: 5820
Covered Lines: 5463
Line Coverage: 93.87%
Coverage Strength: 0.94 hits per line

💛 - Coveralls

Add tests exercising the two error branches introduced in this branch:
AdvancedHandler converting a raising command handler into an ADS device
error, and AdsClientConnection surviving a client RST mid-request. Restores
the coverage the robustness commit dropped by adding untested branches.
@yannS2016
yannS2016 force-pushed the fix/testserver-robustness branch from 4c5a6d9 to 7056f37 Compare June 23, 2026 18:37
@yannS2016

Copy link
Copy Markdown
Contributor Author

Added some test for the feature and fixes mentions in the PR

@stlehmann stlehmann left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for tracking these down — the root causes are all correctly diagnosed (class-vs-instance counter, unguarded recvfrom, uncaught handler exception), and the fixes match real TwinCAT behavior. A few smaller things worth a look before merge:

1. advanced_handler.pySUMUP_WRITE (~L357-378) + new catch-all (~L497)
One bad sub-request in a SUMUP_WRITE batch now returns a single 0x0700 batch error — but earlier sub-writes (and their notification callbacks) already landed before the exception hit. Real ADS reports one error code per sub-request: adsSyncReadWriteReqEx2 already allocates index_offset * 4 response bytes for exactly this, and adsSumWriteBytes/adsSumRead already parse it that way. The SUMUP_WRITE branch already builds a same-shaped array, just hardcoded to all-zeros — filling it with real per-item codes on a per-iteration catch would match both the protocol and pyads' own client parsing.

2. testserver.py (~L197-202)
The new except (ConnectionResetError, OSError) around recvfrom swallows the error with no log line, unlike the dispatch-level catch in advanced_handler.py which calls logger.exception(...). A real socket bug would now vanish silently — worth a log line here too.

3. Minor, same lineConnectionResetError is already an OSError subclass, so the tuple is redundant with just OSError.

4. Minor, advanced_handler.py (~L502)0x0700 is correct (1792 = ADS "device error" class, per errorcodes.py), just unnamed — pyads has no named error-code constants anywhere yet, and the file's existing "unknown command" line has the same bare-literal style. Worth a follow-up constant, not a blocker.


Generated by Claude Code

- SUMUP_WRITE now returns one ADS error code per sub-request instead of
  letting a single bad sub-write bubble to the dispatch catch-all as one
  collapsed batch error. Successful sub-writes still land, and the
  per-item array is exactly what pyads' own adsSumWriteBytes parses.
- The recvfrom guard now logs via logger.exception (matching the dispatch
  catch) so a genuine socket fault is not swallowed silently, and catches
  OSError only, since ConnectionResetError is already an OSError subclass.
- Add a handler-level regression test for the per-item SUMUP_WRITE codes
  (the client resolves handles first, so a per-item write failure is not
  reachable through pyads.Connection).

0x0700 is left as a bare literal to match the file's existing style;
@yannS2016

yannS2016 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for tracking these down — the root causes are all correctly diagnosed (class-vs-instance counter, unguarded recvfrom, uncaught handler exception), and the fixes match real TwinCAT behavior. A few smaller things worth a look before merge:

1. advanced_handler.pySUMUP_WRITE (~L357-378) + new catch-all (~L497) One bad sub-request in a SUMUP_WRITE batch now returns a single 0x0700 batch error — but earlier sub-writes (and their notification callbacks) already landed before the exception hit. Real ADS reports one error code per sub-request: adsSyncReadWriteReqEx2 already allocates index_offset * 4 response bytes for exactly this, and adsSumWriteBytes/adsSumRead already parse it that way. The SUMUP_WRITE branch already builds a same-shaped array, just hardcoded to all-zeros — filling it with real per-item codes on a per-iteration catch would match both the protocol and pyads' own client parsing.

2. testserver.py (~L197-202) The new except (ConnectionResetError, OSError) around recvfrom swallows the error with no log line, unlike the dispatch-level catch in advanced_handler.py which calls logger.exception(...). A real socket bug would now vanish silently — worth a log line here too.

3. Minor, same lineConnectionResetError is already an OSError subclass, so the tuple is redundant with just OSError.

4. Minor, advanced_handler.py (~L502)0x0700 is correct (1792 = ADS "device error" class, per errorcodes.py), just unnamed — pyads has no named error-code constants anywhere yet, and the file's existing "unknown command" line has the same bare-literal style. Worth a follow-up constant, not a blocker.

Generated by Claude Code

Thanks, all addressed in the latest commit:

  1. SUMUP_WRITE now handles each sub-write independently and returns one error code per sub-request (0 / 0x0700); successful writes still land, and the array matches adsSumWriteBytes. Added a handler-level regression test.
  2. The recvfrom guard now logs via logger.exception like the dispatch catch, so a real socket fault isn't swallowed.
  3. Narrowed to except OSError (dropped the redundant ConnectionResetError). Left 0x0700 as a bare literal to match the file's existing style.

@stlehmann
stlehmann merged commit 5db7973 into stlehmann:master Jul 2, 2026
13 of 14 checks passed
@stlehmann

Copy link
Copy Markdown
Owner

Great work, thanks for your contribution @yannS2016

@yannS2016
yannS2016 deleted the fix/testserver-robustness branch July 2, 2026 21:12
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