Search code examples
windowsgowinget

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


  • exec.Command() works for executing C:\Windows\System32\notepad.exe
  • But exec.Command() doesn't work for executing C:\Users\<username>\AppData\Local\Microsoft\WindowsApps\winget.exe. Fails with the error message:
    • exec: "C:\\Users\\<username>\\AppData\\Local\\Microsoft\\WindowsApps\\winget.exe": file does not exist
  • However, os.StartProcess() works for executing C:\Users\<username>\AppData\Local\Microsoft\WindowsApps\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

    • Workaround is to use os.StartProces() - a lower level API which can be a bit painful to use especially when compared to os.Exec().
      Important: In os.StartProcess(), the argv slice will become os.Args in the new process, so you should normally pass the program name as the first argument:
    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
    
    • Another approach to work around this bug is to (create and) execute a .cmd file (for example) which would (correctly resolve and) execute the file with reparse points. See this (and also this directory) for an example.