windowsgowinget

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


Can someone tell me why?

This code fragment does not work. winget.exe isn't launched.

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

But this works:

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
go version go1.18 windows/amd64

Solution

  • Bug in Golang

    So apparently this is a bug in the Windows implementation of Go and has been filed on GitHub many times - the oldest I could find is this issue which was filed years ago.

    The bug is caused by the fact that exec.Command() internally uses os.Stat() which does not read files with reparse points correctly. os.Lstat() can.

    Windows Store apps use App Execution Aliases, which are essentially zero-byte files with reparse points. This post has some additional details.

    Workarounds

    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