在 Go 中,为什么 exec.Command() 失败但 os.StartProcess() 成功启动 "winget.exe"?

In Go, why does exec.Command() fail but os.StartProcess() succeed launching "winget.exe"?

谁能告诉我为什么?

此代码片段无效。 winget.exe 未启动。

wingetPath := filepath.Join(os.Getenv("LOCALAPPDATA"),
    "Microsoft\WindowsApps\winget.exe")
cmd := exec.Command(wingetPath, "--version")
err := cmd.Start()
fmt.Println(err)
// exec: "C:\Users\<username>\AppData\Local\Microsoft\WindowsApps\winget.exe": file does not exist

但这行得通:

wingetPath := filepath.Join(os.Getenv("LOCALAPPDATA"),
    "Microsoft\WindowsApps\winget.exe")
procAttr := new(os.ProcAttr)
procAttr.Files = []*os.File{nil, nil, nil}

// The argv slice will become os.Args in the new process,
// so it normally starts with the program name
_, err := os.StartProcess(wingetPath, []string{wingetPath, "--version"}, procAttr)
fmt.Println(err)
// <nil>

转到版本:

> go version
go version go1.18 windows/amd64

Golang 中的错误

很明显,这是 Go 的 Windows 实现中的一个错误,并且已在 GitHub 上多次提交 - 我能找到的最早的是 issue这是几年前提交的。

错误是由于 exec.Command() 内部 uses os.Stat() which does not read files with reparse points 正确造成的。 os.Lstat()可以。

Windows 商店应用使用 App Execution Aliases, which are essentially zero-byte files with reparse points. This post 有一些额外的细节。

解决方法

  • 解决方法是使用 os.StartProces() - 较低级别 API,使用起来可能有点痛苦,尤其是与 os.Exec() 相比时。
    重要:在os.StartProcess()中,argv slice在新进程中会变成os.Args,所以一般应该把程序名作为第一个传递参数:
wingetPath := filepath.Join(os.Getenv("LOCALAPPDATA"),
    "Microsoft\WindowsApps\winget.exe")
procAttr := new(os.ProcAttr)
procAttr.Files = []*os.File{nil, nil, nil}
/*
To redirect IO, pass in stdin, stdout, stderr as required
procAttr.Files = []*os.File{os.Stdin, os.Stdout, os.Stderr}
*/

args = []string { "install", "git.git" }

// The argv slice will become os.Args in the new process,
// so it normally starts with the program name
proc, err := os.StartProcess(wingetPath,
   append([]string{wingetPath}, arg...), procAttr)
fmt.Println(err) // nil
  • 另一种解决此错误的方法是(创建并)执行一个 .cmd 文件(例如),该文件将(正确地解析并)执行具有重新分析点的文件。有关示例,请参见 this (and also this directory)。