初始化
This commit is contained in:
77
platform/command.go
Normal file
77
platform/command.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
// CommandError 保存后台命令失败时的原始诊断信息。
|
||||
// 服务层会把这些输出映射成稳定错误码,界面再按当前语言展示友好提示。
|
||||
type CommandError struct {
|
||||
Command string
|
||||
Args []string
|
||||
ExitCode int
|
||||
Output string
|
||||
}
|
||||
|
||||
func (e *CommandError) Error() string {
|
||||
if strings.TrimSpace(e.Output) != "" {
|
||||
return strings.TrimSpace(e.Output)
|
||||
}
|
||||
if e.ExitCode != 0 {
|
||||
return e.Command + " exited with code " + strconv.Itoa(e.ExitCode)
|
||||
}
|
||||
return e.Command + " failed"
|
||||
}
|
||||
|
||||
// RunHidden 在后台执行命令并合并 stdout/stderr。
|
||||
// Windows 下由平台文件设置 HideWindow 和 CREATE_NO_WINDOW,避免 Git/WSL 查询弹出终端窗口。
|
||||
func RunHidden(ctx context.Context, name string, args ...string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
configureHidden(cmd)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
e := cmd.Run()
|
||||
out := strings.TrimSpace(strings.ReplaceAll(decodeOutput(stdout.Bytes()), "\x00", ""))
|
||||
errOut := strings.TrimSpace(strings.ReplaceAll(decodeOutput(stderr.Bytes()), "\x00", ""))
|
||||
if e != nil {
|
||||
if out == "" {
|
||||
out = errOut
|
||||
} else if errOut != "" {
|
||||
out += "\n" + errOut
|
||||
}
|
||||
if out == "" {
|
||||
out = e.Error()
|
||||
}
|
||||
exitCode := -1
|
||||
var exit *exec.ExitError
|
||||
if errors.As(e, &exit) {
|
||||
exitCode = exit.ExitCode()
|
||||
}
|
||||
return "", &CommandError{Command: name, Args: args, ExitCode: exitCode, Output: out}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// DecodeWSLList 兼容部分 Windows 版本返回的 UTF-16LE 发行版列表。
|
||||
func DecodeWSLList(b []byte) string { return decodeOutput(b) }
|
||||
|
||||
func decodeOutput(b []byte) string {
|
||||
if len(b) >= 2 && (b[1] == 0 || b[0] == 0xff && b[1] == 0xfe) {
|
||||
if len(b)%2 != 0 {
|
||||
b = b[:len(b)-1]
|
||||
}
|
||||
u := make([]uint16, 0, len(b)/2)
|
||||
for i := 0; i+1 < len(b); i += 2 {
|
||||
u = append(u, uint16(b[i])|uint16(b[i+1])<<8)
|
||||
}
|
||||
return strings.TrimPrefix(string(utf16.Decode(u)), "\ufeff")
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
45
platform/path.go
Normal file
45
platform/path.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Package platform 封装操作系统差异,包括隐藏子进程和 WSL 路径转换。
|
||||
package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PathDescriptor 是项目路径的标准描述。WindowsPath 用于 Go 文件扫描,LinuxPath 用于 WSL 内 Git。
|
||||
type PathDescriptor struct {
|
||||
Original string
|
||||
WindowsPath string
|
||||
Distro string
|
||||
LinuxPath string
|
||||
WSL bool
|
||||
}
|
||||
|
||||
// ResolvePath 识别两种 WSL UNC 前缀,并转换为 WSL 内部可识别的 Linux 绝对路径。
|
||||
func ResolvePath(raw string) (PathDescriptor, error) {
|
||||
p := filepath.Clean(strings.TrimSpace(raw))
|
||||
if p == "" {
|
||||
return PathDescriptor{}, errors.New("PATH_REQUIRED")
|
||||
}
|
||||
slash := strings.ReplaceAll(p, "/", "\\")
|
||||
lower := strings.ToLower(slash)
|
||||
prefix := ""
|
||||
switch {
|
||||
case strings.HasPrefix(lower, "\\\\wsl.localhost\\"):
|
||||
prefix = "\\\\wsl.localhost\\"
|
||||
case strings.HasPrefix(lower, "\\\\wsl$\\"):
|
||||
prefix = "\\\\wsl$\\"
|
||||
default:
|
||||
return PathDescriptor{Original: raw, WindowsPath: p}, nil
|
||||
}
|
||||
rest := strings.TrimPrefix(slash, prefix)
|
||||
parts := strings.Split(rest, "\\")
|
||||
if len(parts) < 2 || parts[0] == "" {
|
||||
return PathDescriptor{}, errors.New("WSL_PATH_INVALID")
|
||||
}
|
||||
distro := parts[0]
|
||||
linux := "/" + strings.Join(parts[1:], "/")
|
||||
canonical := "\\\\wsl.localhost\\" + distro + "\\" + strings.Join(parts[1:], "\\")
|
||||
return PathDescriptor{Original: raw, WindowsPath: canonical, Distro: distro, LinuxPath: linux, WSL: true}, nil
|
||||
}
|
||||
25
platform/path_test.go
Normal file
25
platform/path_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package platform
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestResolveWSLPaths(t *testing.T) {
|
||||
cases := []struct{ input, distro, linux string }{{`\\wsl.localhost\Ubuntu-24.04\home\lee\app`, "Ubuntu-24.04", "/home/lee/app"}, {`\\wsl$\Debian\opt\site`, "Debian", "/opt/site"}}
|
||||
for _, c := range cases {
|
||||
d, e := ResolvePath(c.input)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if !d.WSL || d.Distro != c.distro || d.LinuxPath != c.linux {
|
||||
t.Fatalf("%q => %#v", c.input, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestResolveWindowsPath(t *testing.T) {
|
||||
d, e := ResolvePath(`D:\code\app`)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if d.WSL {
|
||||
t.Fatal("windows path detected as WSL")
|
||||
}
|
||||
}
|
||||
7
platform/process_other.go
Normal file
7
platform/process_other.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !windows
|
||||
|
||||
package platform
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func configureHidden(_ *exec.Cmd) {}
|
||||
13
platform/process_windows.go
Normal file
13
platform/process_windows.go
Normal file
@@ -0,0 +1,13 @@
|
||||
//go:build windows
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// configureHidden 同时设置 HideWindow 和 CREATE_NO_WINDOW,避免 git/wsl 查询闪出终端窗口。
|
||||
func configureHidden(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: 0x08000000}
|
||||
}
|
||||
16
platform/process_windows_test.go
Normal file
16
platform/process_windows_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
//go:build windows
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigureHidden(t *testing.T) {
|
||||
c := exec.Command("cmd.exe", "/c", "exit", "0")
|
||||
configureHidden(c)
|
||||
if c.SysProcAttr == nil || !c.SysProcAttr.HideWindow || c.SysProcAttr.CreationFlags&0x08000000 == 0 {
|
||||
t.Fatalf("hidden process flags missing: %#v", c.SysProcAttr)
|
||||
}
|
||||
}
|
||||
35
platform/wsl.go
Normal file
35
platform/wsl.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ListWSLDistros 返回本机已安装的 WSL 发行版;未安装 WSL 时返回稳定错误码。
|
||||
func ListWSLDistros(ctx context.Context) ([]string, error) {
|
||||
if _, e := exec.LookPath("wsl.exe"); e != nil {
|
||||
return nil, &PlatformError{Code: "WSL_NOT_INSTALLED", Detail: e.Error()}
|
||||
}
|
||||
out, e := RunHidden(ctx, "wsl.exe", "--list", "--quiet")
|
||||
if e != nil {
|
||||
return nil, &PlatformError{Code: "WSL_LIST_FAILED", Detail: e.Error()}
|
||||
}
|
||||
items := []string{}
|
||||
for _, v := range strings.FieldsFunc(out, func(r rune) bool { return r == '\r' || r == '\n' || r == 0 }) {
|
||||
v = strings.TrimSpace(v)
|
||||
if v != "" {
|
||||
items = append(items, v)
|
||||
}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
type PlatformError struct{ Code, Detail string }
|
||||
|
||||
func (e *PlatformError) Error() string {
|
||||
if e.Detail == "" {
|
||||
return e.Code
|
||||
}
|
||||
return e.Code + ": " + e.Detail
|
||||
}
|
||||
Reference in New Issue
Block a user