fix(download_to_device): port login() probe + loginWaitSeconds from connect
Same SP21+/SP22 login() drift as connect_to_device: - 'TryOnlineChange' is gone from OnlineChangeOption - login() now requires (OnlineChangeOption, bool) -- two positional The fork hard-coded the old call shape; observed today on SP22 Patch 1 with the runtime up and the project compiled, every download_to_device attempt failed with "login() takes exactly 2 arguments (0 given)". Fix mirrorse862846/eee8ce2: - Probe OnlineChangeOption members at runtime (priority order tuned for download: WithDownload / ForceDownload come first, since 'download' implies a write). - Try (enum, False) / (enum, True) / (enum,) / bool / no-arg shapes. - Add LOGIN_WAIT_SECONDS post-login state-stabilisation poll for the credential dialog (default 60s, configurable 0-600). - Tool-side IPC timeout = waitSec + 120s headroom for the actual download work (download is heavier than connect, hence 120 vs 30). The download itself is still done via the existing fall-through: online_app.download() if exposed, else create_boot_application(). On SP22 the dir() shows source_download + create_boot_application (no 'download'), so the create_boot_application path is what runs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
64906c4ab5
commit
b3bf4a83d5
2 changed files with 82 additions and 18 deletions
|
|
@ -1,28 +1,84 @@
|
|||
import sys, scriptengine as script_engine, os, traceback
|
||||
|
||||
LOGIN_WAIT_SECONDS = {LOGIN_WAIT_SECONDS}
|
||||
|
||||
try:
|
||||
print("DEBUG: download_to_device script: Project='%s'" % PROJECT_FILE_PATH)
|
||||
print("DEBUG: download_to_device script: Project='%s', LoginWaitSec=%d" % (
|
||||
PROJECT_FILE_PATH, LOGIN_WAIT_SECONDS))
|
||||
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
||||
|
||||
online_app, target_app = ensure_online_connection(primary_project)
|
||||
app_name = getattr(target_app, 'get_name', lambda: "Unknown")()
|
||||
|
||||
# Login with online change option if available, then download
|
||||
# Login. Same SP-version drift as connect_to_device: TryOnlineChange
|
||||
# is gone, login() now requires (OnlineChangeOption, bool) on SP21+.
|
||||
# Use the same defensive probe + post-login wait pattern.
|
||||
print("DEBUG: Logging in for download...")
|
||||
|
||||
if hasattr(online_app, 'login'):
|
||||
if hasattr(script_engine, 'OnlineChangeOption'):
|
||||
try:
|
||||
online_app.login(script_engine.OnlineChangeOption.TryOnlineChange)
|
||||
print("DEBUG: Logged in with TryOnlineChange.")
|
||||
except Exception as e:
|
||||
print("DEBUG: Login with OnlineChangeOption failed, trying plain login: %s" % e)
|
||||
online_app.login()
|
||||
else:
|
||||
online_app.login()
|
||||
else:
|
||||
if not hasattr(online_app, 'login'):
|
||||
raise TypeError("Online application does not support login().")
|
||||
|
||||
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)
|
||||
# For download we prefer 'WithDownload' / 'ForceDownload' over 'Try'
|
||||
for preferred in ('WithDownload', 'ForceDownload', 'Try',
|
||||
'TryOnlineChange', 'OnlineChangeOnly', 'None_', 'None'):
|
||||
if preferred in oc_members:
|
||||
try:
|
||||
enum_candidates.append((preferred, getattr(oc, preferred)))
|
||||
except Exception:
|
||||
pass
|
||||
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
|
||||
|
||||
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,)))
|
||||
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("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)
|
||||
|
||||
# Wait for state to stabilise (credential dialog handling)
|
||||
print("DEBUG: login() returned. Waiting up to %d seconds for state to stabilise" % LOGIN_WAIT_SECONDS)
|
||||
STABLE_STATES = ('run', 'stop', 'connected', 'halt', 'breakpoint')
|
||||
for elapsed in range(LOGIN_WAIT_SECONDS):
|
||||
state = "unknown"
|
||||
if hasattr(online_app, 'application_state'):
|
||||
try:
|
||||
state = str(online_app.application_state)
|
||||
except Exception:
|
||||
pass
|
||||
if state.lower() in STABLE_STATES:
|
||||
print("DEBUG: state stabilised at '%s' after %ds" % (state, elapsed))
|
||||
break
|
||||
try:
|
||||
script_engine.system.delay(1000)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Download
|
||||
print("DEBUG: Calling download()...")
|
||||
if hasattr(online_app, 'download'):
|
||||
|
|
|
|||
|
|
@ -996,17 +996,25 @@ export async function startMcpServer(config: ServerConfig): Promise<void> {
|
|||
|
||||
s.tool(
|
||||
'download_to_device',
|
||||
'Downloads the compiled application to the PLC device. Attempts online change first, falls back to full download.',
|
||||
'Downloads the compiled application to the PLC device. Attempts online change first, falls back to full download. Same login-dialog handling as connect_to_device: loginWaitSeconds controls how long the script waits for state stabilisation if a credential dialog pops up.',
|
||||
{
|
||||
projectFilePath: z.string().describe("Path to the project file."),
|
||||
loginWaitSeconds: z.number().int().min(0).max(600).optional().describe("Seconds to wait for application state to stabilise after login() returns. Default: 60. Range 0-600."),
|
||||
},
|
||||
async (args: { projectFilePath: string }) => {
|
||||
async (args: { projectFilePath: string; loginWaitSeconds?: number }) => {
|
||||
const escaped = resolvePath(args.projectFilePath, workspaceDir);
|
||||
const waitSec = args.loginWaitSeconds ?? 60;
|
||||
const script = scriptManager.prepareScriptWithHelpers(
|
||||
'download_to_device', { PROJECT_FILE_PATH: escaped },
|
||||
'download_to_device',
|
||||
{
|
||||
PROJECT_FILE_PATH: escaped,
|
||||
LOGIN_WAIT_SECONDS: String(waitSec),
|
||||
},
|
||||
['ensure_project_open', 'ensure_online_connection']
|
||||
);
|
||||
const result = await executor.executeScript(script, 120_000);
|
||||
// Tool-side timeout = wait window + 120s headroom for the actual download
|
||||
const ipcTimeoutMs = (waitSec + 120) * 1000;
|
||||
const result = await executor.executeScript(script, ipcTimeoutMs);
|
||||
return formatToolResponse(result, `Application downloaded to device for ${args.projectFilePath}.`);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue