mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-19 23:30:11 +08:00
🐛 fix(driver): 修复可选驱动构建时 Go PATH 检测误判 (#353)
## 背景
在 `dev-ac6ef06` 构建中,安装 SQL Server 等可选驱动时,GoNavi 在部分 macOS 环境会误报“当前环境未安装
Go”。
实际问题并非未安装 Go,而是应用从图形界面启动时没有继承终端中的 PATH,导致 `brew` 安装的 Go(如
`/opt/homebrew/bin/go`)无法被 `exec.LookPath("go")` 发现,进而阻塞可选驱动代理的本地构建流程。
材料参考:
<img width="2142" height="1460" alt="连接失败"
src="https://github.com/user-attachments/assets/0844cf97-5720-4677-a806-65e056fa9766"
/>
<img width="289" height="74" alt="image"
src="https://github.com/user-attachments/assets/3e98e482-f74d-4b68-8605-b712fbdb98c1"
/>
## 关键修改
- 为可选驱动源码构建新增 `go` 可执行文件解析逻辑,避免仅依赖当前进程 PATH
- 增加常见 Go 安装路径兜底:
- `/opt/homebrew/bin/go`
- `/usr/local/go/bin/go`
- `/usr/local/bin/go`
- 在常见路径未命中时,再回退到登录 shell 中执行 `command -v go`
- 解析 shell 输出时逐行筛选真实存在的路径,避免 shell 启动脚本输出额外提示导致误判
- 为 Go 探测逻辑补充单元测试,覆盖:
- shell 列表去重与顺序
- 常见路径回退
- shell 回退
- 噪音输出过滤
## 影响范围
- 仅影响可选驱动代理的源码构建阶段
- 不影响已内置驱动
- 不影响普通数据库连接、前端界面和其他业务逻辑
- 主要改善 macOS 图形界面启动应用时的 Go 环境探测兼容性
## 验证方式
已执行:
```bash
go test ./internal/app
```
## 修复效果
<img width="1114" height="784" alt="已连接1"
src="https://github.com/user-attachments/assets/72f4bb89-6c0b-4632-9098-3ce5b865e288"
/>
<img width="1032" height="791" alt="已连接2"
src="https://github.com/user-attachments/assets/6330cff2-c13b-4a9b-852d-8fc234819f81"
/>
## 验证点:
- 终端内已安装 Go 且可执行时,保持现有行为
- GUI 进程未继承 PATH 时,可通过常见目录或 shell 回退找到 Go
- shell 启动脚本存在额外输出时,仍可解析到真实 Go 路径
## 风险与回滚
### 风险:
- 仅新增本地命令探测与路径兜底逻辑,影响面较小
- 若用户使用非常规 Go 安装方式,仍可能需要后续补充手动指定 Go 路径的正式方案
### 回滚:
- 可直接回退本 PR 中 internal/app/methods_driver.go 与对应测试变更
## 备注
当前使用中还观察到“驱动下载链路域名不可达”在已有网络代理时可能出现误报,但该问题既不影响当前 PATH
修复的有效性,也并不阻塞下载,所以未纳入本次修改范围。
This commit is contained in:
@@ -2,6 +2,7 @@ package app
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
@@ -29,6 +30,86 @@ import (
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
var (
|
||||
goBinaryLookPath = exec.LookPath
|
||||
goBinaryStat = os.Stat
|
||||
goBinaryCommand = func(name string, arg ...string) *exec.Cmd {
|
||||
return exec.Command(name, arg...)
|
||||
}
|
||||
)
|
||||
|
||||
// resolveGoBinaryPath 定位 Go 可执行文件,兼容 macOS 图形应用未继承 shell PATH 的场景 by AI.Coding
|
||||
func resolveGoBinaryPath() (string, error) {
|
||||
if goPath, err := goBinaryLookPath("go"); err == nil {
|
||||
return goPath, nil
|
||||
}
|
||||
|
||||
// 修复点:GUI 进程常拿不到终端里的 PATH,这里补充常见安装位置兜底。
|
||||
commonCandidates := []string{
|
||||
"/opt/homebrew/bin/go",
|
||||
"/usr/local/go/bin/go",
|
||||
"/usr/local/bin/go",
|
||||
}
|
||||
for _, candidate := range commonCandidates {
|
||||
if info, err := goBinaryStat(candidate); err == nil && !info.IsDir() {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, shell := range candidateShellsForCommandLookup() {
|
||||
cmd := goBinaryCommand(shell, "-lc", "command -v go")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
goPath := resolveExistingPathFromCommandOutput(output)
|
||||
if goPath == "" {
|
||||
continue
|
||||
}
|
||||
if info, err := goBinaryStat(goPath); err == nil && !info.IsDir() {
|
||||
return goPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", exec.ErrNotFound
|
||||
}
|
||||
|
||||
// resolveExistingPathFromCommandOutput 从命令输出中提取真实存在的路径,避免 shell 启动脚本输出污染探测结果 by AI.Coding
|
||||
func resolveExistingPathFromCommandOutput(value []byte) string {
|
||||
for _, line := range bytes.Split(value, []byte{'\n'}) {
|
||||
trimmed := strings.TrimSpace(string(line))
|
||||
if trimmed != "" {
|
||||
if info, err := goBinaryStat(trimmed); err == nil && !info.IsDir() {
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// candidateShellsForCommandLookup 返回可能可用的 shell,用于回收用户登录环境中的 PATH by AI.Coding
|
||||
func candidateShellsForCommandLookup() []string {
|
||||
seen := make(map[string]struct{}, 4)
|
||||
result := make([]string, 0, 4)
|
||||
appendShell := func(value string) {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[trimmed]; ok {
|
||||
return
|
||||
}
|
||||
seen[trimmed] = struct{}{}
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
|
||||
appendShell(os.Getenv("SHELL"))
|
||||
appendShell("/bin/zsh")
|
||||
appendShell("/bin/bash")
|
||||
appendShell("/bin/sh")
|
||||
return result
|
||||
}
|
||||
|
||||
type driverDefinition struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
@@ -3182,7 +3263,7 @@ func downloadOptionalDriverAgentFromBundle(a *App, definition driverDefinition,
|
||||
func buildOptionalDriverAgentFromSource(definition driverDefinition, executablePath string, selectedVersion string) (string, error) {
|
||||
driverType := normalizeDriverType(definition.Type)
|
||||
displayName := resolveDriverDisplayName(definition)
|
||||
goPath, lookErr := exec.LookPath("go")
|
||||
goPath, lookErr := resolveGoBinaryPath()
|
||||
if lookErr != nil {
|
||||
return "", fmt.Errorf("当前环境未安装 Go,且未找到可用的 %s 预编译代理包", displayName)
|
||||
}
|
||||
|
||||
156
internal/app/methods_driver_test.go
Normal file
156
internal/app/methods_driver_test.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCandidateShellsForCommandLookup_DedupesAndPreservesOrder(t *testing.T) {
|
||||
originalShell := os.Getenv("SHELL")
|
||||
t.Cleanup(func() {
|
||||
_ = os.Setenv("SHELL", originalShell)
|
||||
})
|
||||
if err := os.Setenv("SHELL", "/bin/zsh"); err != nil {
|
||||
t.Fatalf("set SHELL: %v", err)
|
||||
}
|
||||
|
||||
got := candidateShellsForCommandLookup()
|
||||
want := []string{"/bin/zsh", "/bin/bash", "/bin/sh"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("unexpected shell count: got %v want %v", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("unexpected shell order: got %v want %v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveGoBinaryPath_FallsBackToKnownLocation(t *testing.T) {
|
||||
originalLookPath := goBinaryLookPath
|
||||
originalStat := goBinaryStat
|
||||
originalCommand := goBinaryCommand
|
||||
t.Cleanup(func() {
|
||||
goBinaryLookPath = originalLookPath
|
||||
goBinaryStat = originalStat
|
||||
goBinaryCommand = originalCommand
|
||||
})
|
||||
|
||||
goBinaryLookPath = func(file string) (string, error) {
|
||||
return "", exec.ErrNotFound
|
||||
}
|
||||
goBinaryStat = func(name string) (os.FileInfo, error) {
|
||||
if name == "/opt/homebrew/bin/go" {
|
||||
return fakeFileInfo{name: "go"}, nil
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
goBinaryCommand = func(name string, arg ...string) *exec.Cmd {
|
||||
t.Fatalf("shell fallback should not run when common path exists")
|
||||
return nil
|
||||
}
|
||||
|
||||
got, err := resolveGoBinaryPath()
|
||||
if err != nil {
|
||||
t.Fatalf("resolveGoBinaryPath returned error: %v", err)
|
||||
}
|
||||
if got != "/opt/homebrew/bin/go" {
|
||||
t.Fatalf("unexpected go path: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveGoBinaryPath_FallsBackToShellOutput(t *testing.T) {
|
||||
originalLookPath := goBinaryLookPath
|
||||
originalStat := goBinaryStat
|
||||
originalCommand := goBinaryCommand
|
||||
originalShell := os.Getenv("SHELL")
|
||||
t.Cleanup(func() {
|
||||
goBinaryLookPath = originalLookPath
|
||||
goBinaryStat = originalStat
|
||||
goBinaryCommand = originalCommand
|
||||
_ = os.Setenv("SHELL", originalShell)
|
||||
})
|
||||
if err := os.Setenv("SHELL", "/custom/shell"); err != nil {
|
||||
t.Fatalf("set SHELL: %v", err)
|
||||
}
|
||||
|
||||
goBinaryLookPath = func(file string) (string, error) {
|
||||
return "", exec.ErrNotFound
|
||||
}
|
||||
goBinaryStat = func(name string) (os.FileInfo, error) {
|
||||
if name == "/Users/test/go/bin/go" {
|
||||
return fakeFileInfo{name: "go"}, nil
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
var called []string
|
||||
goBinaryCommand = func(name string, arg ...string) *exec.Cmd {
|
||||
called = append(called, name)
|
||||
if len(called) == 1 {
|
||||
return exec.Command("/bin/sh", "-c", "printf 'welcome\\n/Users/test/go/bin/go\\n'")
|
||||
}
|
||||
return exec.Command("/bin/sh", "-c", "exit 1")
|
||||
}
|
||||
|
||||
got, err := resolveGoBinaryPath()
|
||||
if err != nil {
|
||||
t.Fatalf("resolveGoBinaryPath returned error: %v", err)
|
||||
}
|
||||
if got != "/Users/test/go/bin/go" {
|
||||
t.Fatalf("unexpected go path from shell fallback: %s", got)
|
||||
}
|
||||
if len(called) == 0 || called[0] != "/custom/shell" {
|
||||
t.Fatalf("expected shell lookup to start with SHELL env, got %v", called)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveExistingPathFromCommandOutput_SkipsNoiseLines(t *testing.T) {
|
||||
originalStat := goBinaryStat
|
||||
t.Cleanup(func() {
|
||||
goBinaryStat = originalStat
|
||||
})
|
||||
goBinaryStat = func(name string) (os.FileInfo, error) {
|
||||
if name == "/opt/homebrew/bin/go" {
|
||||
return fakeFileInfo{name: "go"}, nil
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
got := resolveExistingPathFromCommandOutput([]byte("\n notice \n /opt/homebrew/bin/go \n /usr/local/bin/go \n"))
|
||||
if got != "/opt/homebrew/bin/go" {
|
||||
t.Fatalf("unexpected parsed path: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeFileInfo struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (f fakeFileInfo) Name() string { return f.name }
|
||||
func (f fakeFileInfo) Size() int64 { return 0 }
|
||||
func (f fakeFileInfo) Mode() os.FileMode { return 0o755 }
|
||||
func (f fakeFileInfo) ModTime() time.Time { return time.Time{} }
|
||||
func (f fakeFileInfo) IsDir() bool { return false }
|
||||
func (f fakeFileInfo) Sys() any { return nil }
|
||||
|
||||
func TestCandidateShellsForCommandLookup_IgnoresBlankShell(t *testing.T) {
|
||||
originalShell := os.Getenv("SHELL")
|
||||
t.Cleanup(func() {
|
||||
_ = os.Setenv("SHELL", originalShell)
|
||||
})
|
||||
if err := os.Setenv("SHELL", " "); err != nil {
|
||||
t.Fatalf("set SHELL: %v", err)
|
||||
}
|
||||
|
||||
got := candidateShellsForCommandLookup()
|
||||
joined := strings.Join(got, ",")
|
||||
if strings.Contains(joined, " ") {
|
||||
t.Fatalf("expected blank shell to be ignored: %v", got)
|
||||
}
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("unexpected shell list: %v", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user