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:
parent
b76acbe227
commit
d0b33f8c71
@ -18,6 +18,47 @@ import io
|
|||||||
|
|
||||||
import numpy as np
|
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,再 libkplus(libkplus 相依 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:
|
try:
|
||||||
import kp
|
import kp
|
||||||
HAS_KP = True
|
HAS_KP = True
|
||||||
|
|||||||
@ -62,9 +62,9 @@ const (
|
|||||||
type PythonMode string
|
type PythonMode string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PythonModeAuto PythonMode = "auto" // 先試 system,失敗才走 bundled(R4 決策:M1 先 system)
|
PythonModeAuto PythonMode = "auto" // 先試 bundled(kp wheel 已預裝),失敗才 fallback system
|
||||||
PythonModeBundled PythonMode = "bundled" // 策略 A:內嵌 python-build-standalone
|
PythonModeBundled PythonMode = "bundled" // 策略 A:內嵌 python-build-standalone
|
||||||
PythonModeSystem PythonMode = "system" // 策略 B:系統 python3
|
PythonModeSystem PythonMode = "system" // 策略 B:系統 python3(需使用者自行 pip install)
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServerStatus 回報給前端。
|
// ServerStatus 回報給前端。
|
||||||
@ -852,23 +852,27 @@ func (p *ServerProcess) stop() {
|
|||||||
//
|
//
|
||||||
// M1 實作狀況:
|
// M1 實作狀況:
|
||||||
// - PythonModeSystem:完整實作(findSystemPython)
|
// - PythonModeSystem:完整實作(findSystemPython)
|
||||||
// - PythonModeAuto:先試 system,失敗才走 bundled
|
// - PythonModeAuto:先試 bundled(含預裝 kp wheel),失敗才 fallback system
|
||||||
// - PythonModeBundled:placeholder,回錯誤(M2 才實作)
|
// - PythonModeBundled:完整實作(ensureBundledPython)
|
||||||
//
|
//
|
||||||
// R5-5a 之後:python 失敗直接擋啟動(沒有模擬回退)。
|
// R5-5a 之後:python 失敗直接擋啟動(沒有模擬回退)。
|
||||||
|
//
|
||||||
|
// 為什麼不先 system:Local-tool 的架構是整包內嵌 Python + KneronPLUS wheel,
|
||||||
|
// 使用者系統 python 絕大多數沒裝 kp,先 system 會拿到可執行但「import kp 失敗」
|
||||||
|
// 的解譯器,導致 detector scan 空、表面卻看似啟動成功。
|
||||||
func (a *App) ensurePythonRuntime(mode PythonMode) (string, PythonMode, error) {
|
func (a *App) ensurePythonRuntime(mode PythonMode) (string, PythonMode, error) {
|
||||||
if a.startupPipeline != nil {
|
if a.startupPipeline != nil {
|
||||||
a.startupPipeline.EmitStageDetail(2, "startup.stage.2.detail.detect", 0)
|
a.startupPipeline.EmitStageDetail(2, "startup.stage.2.detail.detect", 0)
|
||||||
}
|
}
|
||||||
switch mode {
|
switch mode {
|
||||||
case PythonModeAuto:
|
case PythonModeAuto:
|
||||||
if bin, err := a.findSystemPython(); err == nil {
|
|
||||||
return bin, PythonModeSystem, nil
|
|
||||||
}
|
|
||||||
if bin, err := a.ensureBundledPython(); err == nil {
|
if bin, err := a.ensureBundledPython(); err == nil {
|
||||||
return bin, PythonModeBundled, 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:
|
case PythonModeSystem:
|
||||||
bin, err := a.findSystemPython()
|
bin, err := a.findSystemPython()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user