fix(local-tool): Linux udev rule 自動偵測安裝 + pkexec FUSE 修復
兩個修復: 1. pkexec cp 失敗:AppImage 的 FUSE mount(/tmp/.mount_vision*)有特殊 權限,pkexec 提權後的 root process 無法讀取 user mount 的 FUSE 檔案。 修法:先用 os.ReadFile 把 rule 讀到 /tmp/visiona-local-99-kneron.rules (普通使用者權限可寫 /tmp),再 pkexec cp 從 /tmp 到 /etc/udev/rules.d/。 同時修 server API endpoint 和 Wails 啟動流程兩處。 2. 啟動流程自動偵測 udev:Stage 4 probeDeviceListAndComplete 解析 /api/devices response 的 udevHint 欄位。Linux + 0 裝置 + udevHint=true → 自動找 bundle 裡的 99-kneron.rules → 複製到 /tmp → pkexec cp 安裝 到 /etc/udev/rules.d/ → reload udev。會彈 Linux 圖形化密碼框,使用者 輸入密碼即完成。取消密碼不阻擋啟動流程(log 記錄跳過)。 Wails 控制台 Stage 4 會顯示細步文案:「偵測到 USB 權限未設定,正在 安裝(請在密碼視窗輸入密碼)...」 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
372259b4b1
commit
4e3dc3e504
@ -267,8 +267,22 @@ func (h *SystemHandler) InstallUdevRule(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AppImage FUSE mount 的檔案在 pkexec 提權後無法被 root 直接讀取
|
||||||
|
// (FUSE allow_other 沒開),先 cp 到 /tmp 再 pkexec 從 /tmp 複製到 /etc。
|
||||||
|
tmpRule := "/tmp/visiona-local-99-kneron.rules"
|
||||||
|
if data, err := os.ReadFile(ruleSrc); err == nil {
|
||||||
|
_ = os.WriteFile(tmpRule, data, 0o644)
|
||||||
|
} else {
|
||||||
|
c.JSON(500, gin.H{"success": false, "error": gin.H{
|
||||||
|
"code": "UDEV_READ_FAILED",
|
||||||
|
"message": fmt.Sprintf("cannot read rule file: %v", err),
|
||||||
|
}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpRule)
|
||||||
|
|
||||||
// 用 pkexec 提權複製(會彈 Linux 圖形化密碼對話框)
|
// 用 pkexec 提權複製(會彈 Linux 圖形化密碼對話框)
|
||||||
cpCmd := exec.Command("pkexec", "cp", ruleSrc, "/etc/udev/rules.d/99-kneron.rules")
|
cpCmd := exec.Command("pkexec", "cp", tmpRule, "/etc/udev/rules.d/99-kneron.rules")
|
||||||
if out, err := cpCmd.CombinedOutput(); err != nil {
|
if out, err := cpCmd.CombinedOutput(); err != nil {
|
||||||
c.JSON(500, gin.H{"success": false, "error": gin.H{
|
c.JSON(500, gin.H{"success": false, "error": gin.H{
|
||||||
"code": "UDEV_INSTALL_FAILED",
|
"code": "UDEV_INSTALL_FAILED",
|
||||||
@ -277,7 +291,7 @@ func (h *SystemHandler) InstallUdevRule(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// reload udev
|
// reload udev(每個指令單獨 pkexec,避免 shell injection)
|
||||||
_ = exec.Command("pkexec", "udevadm", "control", "--reload-rules").Run()
|
_ = exec.Command("pkexec", "udevadm", "control", "--reload-rules").Run()
|
||||||
_ = exec.Command("pkexec", "udevadm", "trigger").Run()
|
_ = exec.Command("pkexec", "udevadm", "trigger").Run()
|
||||||
|
|
||||||
|
|||||||
@ -85,6 +85,7 @@ const dict = {
|
|||||||
'startup.stage.3.detail.waitHealthSlow': '首次啟動較久屬正常,Windows Defender 掃描可能需 1-2 分鐘...',
|
'startup.stage.3.detail.waitHealthSlow': '首次啟動較久屬正常,Windows Defender 掃描可能需 1-2 分鐘...',
|
||||||
// Stage 4 - 偵測 Kneron 裝置
|
// Stage 4 - 偵測 Kneron 裝置
|
||||||
'startup.stage.4.detail.probe': '正在掃描 USB 裝置...',
|
'startup.stage.4.detail.probe': '正在掃描 USB 裝置...',
|
||||||
|
'startup.stage.4.detail.udev': '偵測到 USB 權限未設定,正在安裝(請在密碼視窗輸入密碼)...',
|
||||||
// Stage 5 - 開啟瀏覽器
|
// Stage 5 - 開啟瀏覽器
|
||||||
'startup.stage.5.detail.open': '正在開啟系統預設瀏覽器...',
|
'startup.stage.5.detail.open': '正在開啟系統預設瀏覽器...',
|
||||||
// Stage 6 - 等待 Web UI 連線
|
// Stage 6 - 等待 Web UI 連線
|
||||||
@ -195,6 +196,7 @@ const dict = {
|
|||||||
'startup.stage.3.detail.waitHealthSlow': 'First launch is slow — Windows Defender scan may take 1-2 minutes...',
|
'startup.stage.3.detail.waitHealthSlow': 'First launch is slow — Windows Defender scan may take 1-2 minutes...',
|
||||||
// Stage 4
|
// Stage 4
|
||||||
'startup.stage.4.detail.probe': 'Scanning USB devices...',
|
'startup.stage.4.detail.probe': 'Scanning USB devices...',
|
||||||
|
'startup.stage.4.detail.udev': 'USB permissions not configured, installing (please enter password)...',
|
||||||
// Stage 5
|
// Stage 5
|
||||||
'startup.stage.5.detail.open': 'Opening system default browser...',
|
'startup.stage.5.detail.open': 'Opening system default browser...',
|
||||||
// Stage 6
|
// Stage 6
|
||||||
|
|||||||
@ -18,6 +18,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -26,6 +27,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@ -763,16 +765,96 @@ func (a *App) probeDeviceListAndComplete(port int) {
|
|||||||
url := fmt.Sprintf("http://127.0.0.1:%d/api/devices", port)
|
url := fmt.Sprintf("http://127.0.0.1:%d/api/devices", port)
|
||||||
client := &http.Client{Timeout: 2 * time.Second}
|
client := &http.Client{Timeout: 2 * time.Second}
|
||||||
resp, err := client.Get(url)
|
resp, err := client.Get(url)
|
||||||
if resp != nil {
|
|
||||||
|
// Linux udev rule 自動偵測:解析 response body 檢查 udevHint
|
||||||
|
if runtime.GOOS == "linux" && resp != nil && resp.StatusCode == 200 {
|
||||||
|
a.checkAndInstallUdevRule(resp)
|
||||||
|
} else if resp != nil {
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
}
|
}
|
||||||
// 不論 err 或 status,都視為階段 4 完成(只是 probe)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "[visiona-local] startup stage 4: device probe non-fatal error: %v\n", err)
|
a.appLog("startup stage 4: device probe non-fatal error: %v", err)
|
||||||
}
|
}
|
||||||
a.startupPipeline.CompleteStage(4)
|
a.startupPipeline.CompleteStage(4)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkAndInstallUdevRule 在 Linux 啟動流程中偵測 udev rule 是否需要安裝。
|
||||||
|
// 解析 /api/devices response body,如果 udevHint=true → 自動嘗試安裝:
|
||||||
|
// 1. 先從 bundle 讀取 99-kneron.rules 到 /tmp
|
||||||
|
// 2. 用 pkexec cp 提權複製到 /etc/udev/rules.d/(彈密碼框)
|
||||||
|
// 3. pkexec udevadm reload + trigger
|
||||||
|
// 成功後 appLog 提示使用者拔插裝置。失敗不阻擋啟動流程。
|
||||||
|
func (a *App) checkAndInstallUdevRule(resp *http.Response) {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var body struct {
|
||||||
|
Data struct {
|
||||||
|
UdevHint bool `json:"udevHint"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !body.Data.UdevHint {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
a.appLog("Linux udev rule 未安裝,正在嘗試自動安裝...")
|
||||||
|
a.startupPipeline.EmitStageDetail(4, "startup.stage.4.detail.udev", 0)
|
||||||
|
|
||||||
|
// 找 bundle 裡的 99-kneron.rules
|
||||||
|
ruleSrc := ""
|
||||||
|
if libDir := os.Getenv("VISIONA_BUNDLE_LIB_DIR"); libDir != "" {
|
||||||
|
candidate := filepath.Join(libDir, "99-kneron.rules")
|
||||||
|
if _, err := os.Stat(candidate); err == nil {
|
||||||
|
ruleSrc = candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ruleSrc == "" {
|
||||||
|
candidates := []string{
|
||||||
|
"installer/linux/99-kneron.rules",
|
||||||
|
"../installer/linux/99-kneron.rules",
|
||||||
|
}
|
||||||
|
for _, c := range candidates {
|
||||||
|
if _, err := os.Stat(c); err == nil {
|
||||||
|
abs, _ := filepath.Abs(c)
|
||||||
|
ruleSrc = abs
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ruleSrc == "" {
|
||||||
|
a.appLog("udev rule 來源檔找不到,跳過自動安裝")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppImage FUSE mount 的檔案在 pkexec 提權後無法被 root 讀取,
|
||||||
|
// 先 cp 到 /tmp 再 pkexec 從 /tmp 安裝。
|
||||||
|
tmpRule := "/tmp/visiona-local-99-kneron.rules"
|
||||||
|
data, err := os.ReadFile(ruleSrc)
|
||||||
|
if err != nil {
|
||||||
|
a.appLog("udev rule 讀取失敗:%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(tmpRule, data, 0o644); err != nil {
|
||||||
|
a.appLog("udev rule 寫 /tmp 失敗:%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpRule)
|
||||||
|
|
||||||
|
cpCmd := exec.Command("pkexec", "cp", tmpRule, "/etc/udev/rules.d/99-kneron.rules")
|
||||||
|
if out, err := cpCmd.CombinedOutput(); err != nil {
|
||||||
|
a.appLog("udev rule 安裝失敗(使用者可能取消了密碼輸入):%v (%s)", err, strings.TrimSpace(string(out)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = exec.Command("pkexec", "udevadm", "control", "--reload-rules").Run()
|
||||||
|
_ = exec.Command("pkexec", "udevadm", "trigger").Run()
|
||||||
|
|
||||||
|
a.appLog("udev rule 安裝成功。請拔掉 Kneron USB 裝置再重新插入,然後在 Web UI 點「掃描裝置」。")
|
||||||
|
}
|
||||||
|
|
||||||
func closeLogFilesSafe(files ...*os.File) {
|
func closeLogFilesSafe(files ...*os.File) {
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
if f != nil {
|
if f != nil {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user