From 4ca15c11900fd11cb28616b9a18ade4d80b7f1c6 Mon Sep 17 00:00:00 2001 From: Jeremy Shen Date: Thu, 11 Jun 2026 18:00:49 +0800 Subject: [PATCH] fix: detect flash failure from output even when returncode is 0 program_flash can return exit code 0 while outputting errors like 'uboot failed' or 'cannot open'. Previously only returncode was checked, so these silent failures caused the workflow to continue past a failed erase/program step. Now scans combined stdout+stderr for failure keywords (uboot, failed, cannot, abort, invalid, unsupported, timeout) and returns success=False. Also checks returncode != 0 as fallback. --- src/flash_programmer.py | 68 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/src/flash_programmer.py b/src/flash_programmer.py index 8740f10..75c7334 100644 --- a/src/flash_programmer.py +++ b/src/flash_programmer.py @@ -10,6 +10,7 @@ Compatible with Xilinx 2018.3 — 2023.2. from __future__ import annotations import binascii +import re import subprocess from dataclasses import dataclass from pathlib import Path @@ -35,6 +36,27 @@ ProgressCallback = Callable[[str, str], None] | None # ── Helpers ───────────────────────────────────────────────────────── +# Keywords in program_flash output that indicate failure even with rc=0 +_FAIL_KEYWORDS = [ + "uboot", "u-boot", "failed", "cannot", "abort", + "invalid", "unsupported", "timeout", +] + + +def _output_has_error(output: str) -> str: + """Check combined stdout+stderr for known failure keywords. + + Returns the first matching line, or empty string if clean. + """ + lowered = output.lower() + for kw in _FAIL_KEYWORDS: + if kw in lowered: + # Find the actual line + for line in output.splitlines(): + if kw in line.lower(): + return line.strip()[:200] + return "" + def _run_command( cmd: list[str], @@ -167,12 +189,27 @@ def wipe_flash( callback=callback, ) - success = result.returncode == 0 + combined = result.stdout + result.stderr + err_line = _output_has_error(combined) + if err_line: + return FlashResult( + step="wipe", + success=False, + message=f"Flash erase failed: {err_line}", + output=combined[:4000], + ) + if result.returncode != 0: + return FlashResult( + step="wipe", + success=False, + message=f"Flash erase failed (exit {result.returncode})", + output=combined[:4000], + ) return FlashResult( step="wipe", - success=success, - message="Flash erased successfully" if success else "Flash erase failed", - output=(result.stdout + result.stderr)[:4000], + success=True, + message="Flash erased successfully", + output=combined[:4000], ) except subprocess.TimeoutExpired: return FlashResult(step="wipe", success=False, message="Flash erase timed out") @@ -252,12 +289,27 @@ def program_flash_bin( try: result = _run_command(cmd, timeout=config.step_timeouts.get("flash_program", 600), callback=callback) - success = result.returncode == 0 + combined = result.stdout + result.stderr + err_line = _output_has_error(combined) + if err_line: + return FlashResult( + step="program", + success=False, + message=f"Flash programming failed: {err_line}", + output=combined[:4000], + ) + if result.returncode != 0: + return FlashResult( + step="program", + success=False, + message=f"Flash programming failed (exit {result.returncode})", + output=combined[:4000], + ) return FlashResult( step="program", - success=success, - message="Flash programmed successfully" if success else "Flash programming failed", - output=(result.stdout + result.stderr)[:4000], + success=True, + message="Flash programmed successfully", + output=combined[:4000], ) except subprocess.TimeoutExpired: return FlashResult(step="program", success=False, message="Flash programming timed out (600s)")