fix(local-tool): macOS 掃不到 Kneron 裝置 — PythonModeAuto 先 bundled

症狀:Mac 版 app 啟動後,前端顯示沒有裝置(實際 KL520 透過 USB 連上)。

根因:
PythonModeAuto 原本「先 system 後 bundled」,但系統 python3 通常沒裝
KneronPLUS wheel → `import kp` 失敗 → HAS_KP=False → bridge 降級 pyusb →
pyusb 找不到 libusb → scan 空陣列。表面看起來啟動成功但 detector 是空的。

修法:
- visiona-local/app.go PythonModeAuto 語意翻轉 → 先 bundled(已預裝 kp wheel),
  失敗才 fallback system。Local-tool 架構就是整包內嵌 Python + wheels,
  系統 python 不會裝 kp,不該優先。
- server/scripts/kneron_bridge.py 在 `import kp` 前新增
  `_preload_kneron_dylibs_macos()` — 用 ctypes.CDLL 絕對路徑預載 wheel 內
  `kp/lib/libusb-1.0.0.dylib` + `libkplus.dylib`,避開 macOS hardened
  runtime 剝掉 DYLD_LIBRARY_PATH 的風險。Windows/Linux 守門不執行。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jim800121chen 2026-04-21 01:09:56 +08:00
parent b76acbe227
commit d0b33f8c71
2 changed files with 53 additions and 8 deletions

View File

@ -18,6 +18,47 @@ import io
import numpy as np
def _preload_kneron_dylibs_macos():
"""macOS 專用:用絕對路徑預先 dlopen wheel 內的 libusb + libkplus。
背景
- KneronPLUS wheel libusb-1.0.0.dylib + libkplus.dylib 放在 kp/lib/
- macOS dyld 在載入 libkplus 時會去找它的相依 libusb-1.0.0.dylib
預設搜尋路徑/usr/local/lib/usr/lib bundled Python 環境下通常
找不到我們沒有 brew libusb於是 `import kp` 就拋 OSError
HAS_KP=False scan 回空陣列
- macOS hardened runtime 會剝掉 DYLD_LIBRARY_PATH 等環境變數所以
改從 Go 端注入 env 也不保險最穩的做法是在 Python 這端用 ctypes
以絕對路徑先載入後續 `import kp` dyld 會重用已載入的映像
Windows / Linux 不走這支 各自機制已在 Go 端處理Windows PATH
Linux wheel 自帶的 libusb.so.1.0.0 + LD_LIBRARY_PATH
"""
if sys.platform != "darwin":
return
try:
import ctypes
import importlib.util
spec = importlib.util.find_spec("kp")
if spec is None or not spec.submodule_search_locations:
return
kp_dir = spec.submodule_search_locations[0]
lib_dir = os.path.join(kp_dir, "lib")
# 載入順序:先 libusb再 libkpluslibkplus 相依 libusb
for name in ("libusb-1.0.0.dylib", "libkplus.dylib"):
path = os.path.join(lib_dir, name)
if os.path.isfile(path):
try:
ctypes.CDLL(path, mode=ctypes.RTLD_GLOBAL)
except OSError:
pass
except Exception:
pass
_preload_kneron_dylibs_macos()
try:
import kp
HAS_KP = True

View File

@ -62,9 +62,9 @@ const (
type PythonMode string
const (
PythonModeAuto PythonMode = "auto" // 先試 system失敗才走 bundledR4 決策M1 先 system
PythonModeAuto PythonMode = "auto" // 先試 bundledkp wheel 已預裝),失敗才 fallback system
PythonModeBundled PythonMode = "bundled" // 策略 A內嵌 python-build-standalone
PythonModeSystem PythonMode = "system" // 策略 B系統 python3
PythonModeSystem PythonMode = "system" // 策略 B系統 python3(需使用者自行 pip install
)
// ServerStatus 回報給前端。
@ -852,23 +852,27 @@ func (p *ServerProcess) stop() {
//
// M1 實作狀況:
// - PythonModeSystem完整實作findSystemPython
// - PythonModeAuto先試 system失敗才走 bundled
// - PythonModeBundledplaceholder回錯誤M2 才實作
// - PythonModeAuto先試 bundled含預裝 kp wheel失敗才 fallback system
// - PythonModeBundled完整實作ensureBundledPython
//
// R5-5a 之後python 失敗直接擋啟動(沒有模擬回退)。
//
// 為什麼不先 systemLocal-tool 的架構是整包內嵌 Python + KneronPLUS wheel
// 使用者系統 python 絕大多數沒裝 kp先 system 會拿到可執行但「import kp 失敗」
// 的解譯器,導致 detector scan 空、表面卻看似啟動成功。
func (a *App) ensurePythonRuntime(mode PythonMode) (string, PythonMode, error) {
if a.startupPipeline != nil {
a.startupPipeline.EmitStageDetail(2, "startup.stage.2.detail.detect", 0)
}
switch mode {
case PythonModeAuto:
if bin, err := a.findSystemPython(); err == nil {
return bin, PythonModeSystem, nil
}
if bin, err := a.ensureBundledPython(); err == nil {
return bin, PythonModeBundled, nil
}
return "", PythonModeAuto, fmt.Errorf("no python runtime available (tried system + bundled)")
if bin, err := a.findSystemPython(); err == nil {
return bin, PythonModeSystem, nil
}
return "", PythonModeAuto, fmt.Errorf("no python runtime available (tried bundled + system)")
case PythonModeSystem:
bin, err := a.findSystemPython()