0
0
Fork 0

fix(connect_to_device): probe OnlineChangeOption + try login() call shapes

Defensively iterates plausible signatures for IOnlineApplication.login()
across SP versions. Prior version hard-coded
'login(OnlineChangeOption.TryOnlineChange)' which fails on SP21+/SP22:

  - 'TryOnlineChange' enum member was removed
  - login() now requires (OnlineChangeOption, bool) -- two positional args

Concrete failure observed today on SP22 Patch 1:
  - First fallback raised: 'type' object has no attribute 'TryOnlineChange'
  - Second fallback raised: login() takes exactly 2 arguments (0 given)
  ...with the runtime actually reachable on port 11740. So a real call
  was waiting for the right arguments.

New approach:
  1) Discover OnlineChangeOption members at runtime via dir(); print them
     so future debug sessions see exactly what's exposed on this CODESYS.
  2) Try (enum, False) / (enum, True) / (enum,) for each candidate enum
     value, prioritising 'Try'-ish names then 'WithDownload' / others.
  3) Fall back to bool-only and no-arg for very old SPs.
  4) Log every attempt (DEBUG line) so a failure trace shows the full
     matrix that was tried.

Companion fixes for download_to_device, write_variable, etc. coming as
separate commits if their root cause is the same login API drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Karstein Phobic Nyvold Kvistad 2026-04-25 18:16:54 +02:00
parent 9f44a603d8
commit e862846164

View file

@ -7,24 +7,66 @@ try:
online_app, target_app = ensure_online_connection(primary_project)
app_name = getattr(target_app, 'get_name', lambda: "Unknown")()
# Login to the device
# Login to the device. The login() signature shifted across SPs:
# - Older: login() with no args, or login(OnlineChangeOption.TryOnlineChange)
# - SP21+/SP22: login(OnlineChangeOption, bool) -- two required positional
# args, with several enum members renamed (TryOnlineChange removed).
# Probe what's available, then try a sequence of plausible call shapes.
print("DEBUG: Calling login() on online application...")
if hasattr(online_app, 'login'):
# Try with OnlineChangeOption if available
if hasattr(script_engine, 'OnlineChangeOption'):
try:
online_app.login(script_engine.OnlineChangeOption.TryOnlineChange)
print("DEBUG: Logged in with TryOnlineChange option.")
except Exception as e:
print("DEBUG: Login with OnlineChangeOption failed, trying plain login: %s" % e)
online_app.login()
else:
online_app.login()
print("DEBUG: Login successful.")
else:
if not hasattr(online_app, 'login'):
raise TypeError("Online application does not support login().")
# Discover OnlineChangeOption members defensively. Different SPs expose
# different names. Build candidate enum values in priority order.
enum_candidates = []
if hasattr(script_engine, 'OnlineChangeOption'):
oc = script_engine.OnlineChangeOption
oc_members = sorted([m for m in dir(oc) if not m.startswith('_')])
print("DEBUG: OnlineChangeOption members: %s" % oc_members)
# Priority order: prefer "try"-ish (no-download), then "download" variants
for preferred in ('Try', 'TryOnlineChange', 'OnlineChangeOnly',
'WithDownload', 'ForceDownload', 'None_', 'None'):
if preferred in oc_members:
try:
enum_candidates.append((preferred, getattr(oc, preferred)))
except Exception:
pass
# Append all remaining members as fallbacks
for m in oc_members:
if m not in [n for n, _ in enum_candidates]:
try:
enum_candidates.append((m, getattr(oc, m)))
except Exception:
pass
# Build call-shape candidates for login(): a list of (description, args-tuple).
call_shapes = []
for nm, val in enum_candidates:
call_shapes.append(("login(%s, False)" % nm, (val, False)))
call_shapes.append(("login(%s, True)" % nm, (val, True)))
call_shapes.append(("login(%s)" % nm, (val,)))
# Also try plain bools and no-arg as fall-backs (for very old SPs)
call_shapes.append(("login(False)", (False,)))
call_shapes.append(("login(True)", (True,)))
call_shapes.append(("login()", ()))
last_err = None
logged_in = False
for desc, args in call_shapes:
try:
online_app.login(*args)
print("DEBUG: %s succeeded" % desc)
logged_in = True
break
except Exception as e:
last_err = e
# Print only the short error to keep log readable
print("DEBUG: %s failed: %s: %s" % (desc, type(e).__name__, e))
if not logged_in:
raise RuntimeError("All login() call shapes failed. Last error: %s" % last_err)
print("DEBUG: Login successful.")
# Check connection state
state = "connected"
if hasattr(online_app, 'application_state'):