proc: Fix command-line arguments on Windows (#501)

* proc: add tests for command-line arguments

adds tests to make sure command-line arguments are passed to Launch() properly

* proc_windows: pass command-line arguments to CreateProcess()

build command-line arguments according to how the standard library does it and pass the command line along to the actual syscall on Windows.

see discussion in #479

* proc: better testing of cmd-line arguments

* proc_windows: fix a possible error-case with passing just 1 argument

previously, the command line pointer passed to sys.CreateProcess was empty, if we had 0 parameters (len(cmd) == 1, as cmd[0] is the executable, so no cmdlineGo would be created, while with any argument it would as len(cmd) > 1). This might cause problems down the road, so make sure we include the command line every time, even if it seems to work without.

* proc: improve testing of command-line arguments

test that arguments with spaces are passed on correctly and DRY failure/success condition checking in the args test
This commit is contained in:
Matias Lahti 2016-04-22 00:20:38 +03:00 committed by Derek Parker
parent e8cb04303a
commit 60946a759c
3 changed files with 101 additions and 4 deletions

20
_fixtures/testargs.go Normal file

@ -0,0 +1,20 @@
package main
import (
"fmt"
"os"
)
func main() {
// this test expects AT LEAST 1 argument, and the first one needs to be "test".
// second one is optional but if given, it should be "-passFlag"
fmt.Printf("received args %#v\n", os.Args)
if len(os.Args) < 2 {
panic("os.args too short!")
} else if os.Args[1] != "test" {
panic("os.args[1] is not test!")
}
if len(os.Args) >= 3 && os.Args[2] != "pass flag" {
panic("os.args[2] is not \"pass flag\"!")
}
}

@ -43,6 +43,21 @@ func withTestProcess(name string, t testing.TB, fn func(p *Process, fixture prot
fn(p, fixture) fn(p, fixture)
} }
func withTestProcessArgs(name string, t testing.TB, fn func(p *Process, fixture protest.Fixture), args []string) {
fixture := protest.BuildFixture(name)
p, err := Launch(append([]string{fixture.Path}, args...))
if err != nil {
t.Fatal("Launch():", err)
}
defer func() {
p.Halt()
p.Kill()
}()
fn(p, fixture)
}
func getRegisters(p *Process, t *testing.T) Registers { func getRegisters(p *Process, t *testing.T) Registers {
regs, err := p.Registers() regs, err := p.Registers()
if err != nil { if err != nil {
@ -1656,6 +1671,45 @@ func TestPanicBreakpoint(t *testing.T) {
}) })
} }
func TestCmdLineArgs(t *testing.T) {
expectSuccess := func(p *Process, fixture protest.Fixture) {
err := p.Continue()
bp := p.CurrentBreakpoint()
if bp != nil && bp.Name == "unrecovered-panic" {
t.Fatalf("testing args failed on unrecovered-panic breakpoint: %v", p.CurrentBreakpoint)
}
exit, exited := err.(ProcessExitedError)
if !exited {
t.Fatalf("Process did not exit!", err)
} else {
if exit.Status != 0 {
t.Fatalf("process exited with invalid status", exit.Status)
}
}
}
expectPanic := func(p *Process, fixture protest.Fixture) {
p.Continue()
bp := p.CurrentBreakpoint()
if bp == nil || bp.Name != "unrecovered-panic" {
t.Fatalf("not on unrecovered-panic breakpoint: %v", p.CurrentBreakpoint)
}
}
// make sure multiple arguments (including one with spaces) are passed to the binary correctly
withTestProcessArgs("testargs", t, expectSuccess, []string{"test"})
withTestProcessArgs("testargs", t, expectSuccess, []string{"test", "pass flag"})
// check that arguments with spaces are *only* passed correctly when correctly called
withTestProcessArgs("testargs", t, expectPanic, []string{"test pass", "flag"})
withTestProcessArgs("testargs", t, expectPanic, []string{"test", "pass", "flag"})
withTestProcessArgs("testargs", t, expectPanic, []string{"test pass flag"})
// and that invalid cases (wrong arguments or no arguments) panic
withTestProcess("testargs", t, expectPanic)
withTestProcessArgs("testargs", t, expectPanic, []string{"invalid"})
withTestProcessArgs("testargs", t, expectPanic, []string{"test", "invalid"})
withTestProcessArgs("testargs", t, expectPanic, []string{"invalid", "pass flag"})
}
func TestIssue462(t *testing.T) { func TestIssue462(t *testing.T) {
// Stacktrace of Goroutine 0 fails with an error // Stacktrace of Goroutine 0 fails with an error
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {

@ -47,9 +47,6 @@ func Launch(cmd []string) (*Process, error) {
if _, err := os.Stat(argv0Go); err != nil { if _, err := os.Stat(argv0Go); err != nil {
return nil, err return nil, err
} }
argv0, _ := syscall.UTF16PtrFromString(argv0Go)
// Duplicate the stdin/stdout/stderr handles // Duplicate the stdin/stdout/stderr handles
files := []uintptr{uintptr(syscall.Stdin), uintptr(syscall.Stdout), uintptr(syscall.Stderr)} files := []uintptr{uintptr(syscall.Stdin), uintptr(syscall.Stdout), uintptr(syscall.Stderr)}
p, _ := syscall.GetCurrentProcess() p, _ := syscall.GetCurrentProcess()
@ -62,6 +59,32 @@ func Launch(cmd []string) (*Process, error) {
defer syscall.CloseHandle(syscall.Handle(fd[i])) defer syscall.CloseHandle(syscall.Handle(fd[i]))
} }
argv0, err := syscall.UTF16PtrFromString(argv0Go)
if err != nil {
return nil, err
}
// create suitable command line for CreateProcess
// see https://github.com/golang/go/blob/master/src/syscall/exec_windows.go#L326
// adapted from standard library makeCmdLine
// see https://github.com/golang/go/blob/master/src/syscall/exec_windows.go#L86
var cmdLineGo string
if len(cmd) >= 1 {
for _, v := range cmd {
if cmdLineGo != "" {
cmdLineGo += " "
}
cmdLineGo += syscall.EscapeArg(v)
}
}
var cmdLine *uint16
if cmdLineGo != "" {
if cmdLine, err = syscall.UTF16PtrFromString(cmdLineGo); err != nil {
return nil, err
}
}
// Initialize the startup info and create process // Initialize the startup info and create process
si := new(sys.StartupInfo) si := new(sys.StartupInfo)
si.Cb = uint32(unsafe.Sizeof(*si)) si.Cb = uint32(unsafe.Sizeof(*si))
@ -70,7 +93,7 @@ func Launch(cmd []string) (*Process, error) {
si.StdOutput = sys.Handle(fd[1]) si.StdOutput = sys.Handle(fd[1])
si.StdErr = sys.Handle(fd[2]) si.StdErr = sys.Handle(fd[2])
pi := new(sys.ProcessInformation) pi := new(sys.ProcessInformation)
err = sys.CreateProcess(argv0, nil, nil, nil, true, DEBUGONLYTHISPROCESS, nil, nil, si, pi) err = sys.CreateProcess(argv0, cmdLine, nil, nil, true, DEBUGONLYTHISPROCESS, nil, nil, si, pi)
if err != nil { if err != nil {
return nil, err return nil, err
} }