From abbe9d4c0bbf5f5ed5c3a132d4e354f330bde4e4 Mon Sep 17 00:00:00 2001 From: jim800121chen Date: Thu, 16 Apr 2026 17:20:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(local-tool):=20DeviceGroup.=5F=5Fdel=5F=5F?= =?UTF-8?q?=20access=20violation=20=E5=85=A8=E9=9D=A2=E4=BF=AE=E5=BE=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一個 commit (a6a121a) 只修了 script 結束時的 cleanup,但使用者仍在 connect 重試路徑看到 access violation: connect attempt 1 failed → 新 connect attempt 2 → GC 回收 attempt 1 的舊 DeviceGroup → __del__ → kp_disconnect_devices 對已失效的 native handle → OSError: access violation 根因:`_device_group = None` 只是清掉 Python reference,舊物件的 __del__ 會延遲到下一次 GC cycle(可能發生在新 connect call 的 allocation 時), 此時 native handle 已 invalid。 修法: - 新增 `_clear_device_group()` helper:先 kp.core.disconnect_devices 把 native handle 正常釋放(errors silenced),再設 None - 全檔搜 `_device_group = None` 共 12 處,除了初始宣告(L40)和兩個 helper 自身(_clear_device_group / _cleanup)以外全部替換為 _clear_device_group() - 涵蓋所有 code path:connect retry / firmware load reconnect / disconnect handler / reset handler / error fallback Co-Authored-By: Claude Opus 4.6 (1M context) --- local-tool/server/scripts/kneron_bridge.py | 35 ++++++++++++++++------ 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/local-tool/server/scripts/kneron_bridge.py b/local-tool/server/scripts/kneron_bridge.py index 4f4bd95..32357ec 100644 --- a/local-tool/server/scripts/kneron_bridge.py +++ b/local-tool/server/scripts/kneron_bridge.py @@ -38,6 +38,24 @@ except ImportError: # ── Global state ────────────────────────────────────────────────────── _device_group = None + + +def _clear_device_group(): + """Safely disconnect and clear the global _device_group. + + KneronPLUS SDK's DeviceGroup.__del__ calls kp_disconnect_devices on the + native handle, but if the handle is already invalid (failed connect / stale + state) it causes 'OSError: access violation'. By explicitly disconnecting + before setting None, __del__ becomes a no-op on an already-disconnected + handle. All errors are silenced — this is best-effort cleanup. + """ + global _device_group + if _device_group is not None: + try: + kp.core.disconnect_devices(_device_group) + except Exception: + pass + _device_group = None _model_id = None _model_nef = None _model_input_size = 224 # updated on model load @@ -732,7 +750,7 @@ def handle_connect(params): _log(f"KL720: Reconnected after firmware load, pid=0x{target_dev.product_id:04X}, fw={fw_str}") else: _log("WARNING: KL720 firmware files not found. Cannot operate with KDP legacy device.") - _device_group = None + _clear_device_group() return {"error": "KL720 has legacy KDP firmware but KDP2 firmware files not found. " "Run update_kl720_firmware.py to flash KDP2 permanently."} @@ -756,9 +774,8 @@ def handle_connect(params): last_err = None for attempt in range(max_retries): try: - # Clear any stale device group from previous failed attempt - # to prevent DeviceGroup.__del__ access violation during GC. - _device_group = None + # Clear any stale device group from previous failed attempt. + _clear_device_group() if use_without_check: _log(f"{_device_chip}: connect_devices_without_check(usb_port_id={target_dev.usb_port_id}, connectable={target_dev.is_connectable}) attempt {attempt+1}/{max_retries}...") @@ -774,7 +791,7 @@ def handle_connect(params): last_err = None break except Exception as conn_err: - _device_group = None # prevent __del__ crash on stale handle + _clear_device_group() last_err = conn_err _log(f"connect attempt {attempt+1} failed: {conn_err}") if attempt < max_retries - 1: @@ -822,7 +839,7 @@ def handle_connect(params): time.sleep(5) # Reconnect after firmware load (with retry) - _device_group = None + _clear_device_group() for retry in range(3): try: descs = kp.core.scan_devices() @@ -861,7 +878,7 @@ def handle_connect(params): } except Exception as e: - _device_group = None + _clear_device_group() return {"error": str(e)} @@ -870,7 +887,7 @@ def handle_disconnect(params): global _device_group, _model_id, _model_nef, _firmware_loaded global _model_type, _model_input_size, _device_chip - _device_group = None + _clear_device_group() _model_id = None _model_nef = None _model_type = "tiny_yolov3" @@ -906,7 +923,7 @@ def handle_reset(params): # Even if it throws, the device usually does reset. # Clear all state — the device is gone until it re-enumerates. - _device_group = None + _clear_device_group() _model_id = None _model_nef = None _model_type = "tiny_yolov3"