From adfa462029fd6b96b2f73ae06bf7f57cf0256507 Mon Sep 17 00:00:00 2001 From: Daniel Canter Date: Wed, 25 Aug 2021 23:31:06 -0700 Subject: [PATCH 1/2] Add os, os/exec and internal/syscall/execenv packages from stdlib Due to some functionality in the stdlib not being supported and not wanting to reimplement os/exec.Cmd and friends, we need to fork the stdlib. The functionality being mentioned is related to not being able to do some setup with STARTUPINFOEXW and exposing it in a go friendly way in the stdlib isn't really feasible as there's currently no way to create a job object in the stdlib, and the syscall package is locked down. Signed-off-by: Daniel Canter --- internal/stdlib/README.md | 28 + internal/stdlib/execenv/execenv_windows.go | 56 ++ internal/stdlib/itoa/itoa.go | 33 + internal/stdlib/os/exec.go | 176 +++++ internal/stdlib/os/exec/exec.go | 789 +++++++++++++++++++++ internal/stdlib/os/exec/exec_windows.go | 23 + internal/stdlib/os/exec/lp_windows.go | 94 +++ internal/stdlib/os/exec_posix.go | 137 ++++ internal/stdlib/os/exec_windows.go | 181 +++++ internal/stdlib/os/str.go | 39 + internal/stdlib/syscall/exec_windows.go | 420 +++++++++++ 11 files changed, 1976 insertions(+) create mode 100644 internal/stdlib/README.md create mode 100644 internal/stdlib/execenv/execenv_windows.go create mode 100644 internal/stdlib/itoa/itoa.go create mode 100644 internal/stdlib/os/exec.go create mode 100644 internal/stdlib/os/exec/exec.go create mode 100644 internal/stdlib/os/exec/exec_windows.go create mode 100644 internal/stdlib/os/exec/lp_windows.go create mode 100644 internal/stdlib/os/exec_posix.go create mode 100644 internal/stdlib/os/exec_windows.go create mode 100644 internal/stdlib/os/str.go create mode 100644 internal/stdlib/syscall/exec_windows.go diff --git a/internal/stdlib/README.md b/internal/stdlib/README.md new file mode 100644 index 0000000000..bb21944c32 --- /dev/null +++ b/internal/stdlib/README.md @@ -0,0 +1,28 @@ +# Why +Package stdlib is a fork of a small set of packages from the go stdlib and specifically the os, syscall, os/exec, and /internal/syscall/execenv packages. +It exists because the process execution mechanism in the stdlib (os/exec.Cmd and everything in it's call chain all the way down to syscall.StartProcess) +currently don't expose what's necessary to be able to accomplish some things that we need on Windows. This boils down to three things currently. + +1. `exec.Cmd.Start()` calls `exec.LookPath` which looks for certain windows extensions to launch an executable at the path provided and will fail if +one is not found. Although rare, there are cases of binaries with no extension that are perfectly valid and able to be launched by `CreateProcessW`. +Granted this can be worked around by setting PATHEXT to anything with an empty space entry after the colon separator, so this is more of an added +annoyance for the other two reasons. +For example: +```go +os.Setenv("PATHEXT", ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.CPL; ") +``` + +2. For job containers we'd like the ability to launch a process in a job directly as it's created, instead of launching it and assigning it to the +job slightly afterwards. This introduces a small window where the process is unaccounted for and not in the "container". +The desired behavior can be accomplished in Windows by calling [UpdateProcThreadAtrribute](https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-updateprocthreadattribute) +and using the constant `PROC_THREAD_ATTRIBUTE_JOB_LIST`. For the stdlib to support this it would need a new field off of syscall.SysProcAttr to be +able to pass in the job object handle that you'd like the process added to. However, there is no way to create a job object in the stdlib itself +and the syscall package is locked down for the most part. + +3. Almost same story as 2, but in this case we'd like to support assigning a [pseudo console](https://docs.microsoft.com/en-us/windows/console/createpseudoconsole) +to a process. There's no exposed way to pass in the pseudo console handle and there's no syscall package support for making one in the first place. + +The stdlib packages have been modified to use the x/sys/windows package where needed, removed some unneccesary functionality that we +don't need, as well as added the additions described above. + +The fork is based off of go 1.17 with HEAD at a6ff433d6a927e8ad8eaa6828127233296d12ce5. \ No newline at end of file diff --git a/internal/stdlib/execenv/execenv_windows.go b/internal/stdlib/execenv/execenv_windows.go new file mode 100644 index 0000000000..3b79a91104 --- /dev/null +++ b/internal/stdlib/execenv/execenv_windows.go @@ -0,0 +1,56 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build windows +// +build windows + +package execenv + +import ( + "syscall" + "unicode/utf16" + "unsafe" + + "internal/syscall/windows" +) + +// Default will return the default environment +// variables based on the process attributes +// provided. +// +// If the process attributes contain a token, then +// the environment variables will be sourced from +// the defaults for that user token, otherwise they +// will be sourced from syscall.Environ(). +func Default(sys *syscall.SysProcAttr) (env []string, err error) { + if sys == nil || sys.Token == 0 { + return syscall.Environ(), nil + } + var block *uint16 + err = windows.CreateEnvironmentBlock(&block, windows.Token(sys.Token), false) + if err != nil { + return nil, err + } + defer windows.DestroyEnvironmentBlock(block) + blockp := uintptr(unsafe.Pointer(block)) + for { + + // find NUL terminator + end := unsafe.Pointer(blockp) + for *(*uint16)(end) != 0 { + end = unsafe.Pointer(uintptr(end) + 2) + } + + n := (uintptr(end) - uintptr(unsafe.Pointer(blockp))) / 2 + if n == 0 { + // environment block ends with empty string + break + } + + entry := (*[(1 << 30) - 1]uint16)(unsafe.Pointer(blockp))[:n:n] + env = append(env, string(utf16.Decode(entry))) + blockp += 2 * (uintptr(len(entry)) + 1) + } + return +} diff --git a/internal/stdlib/itoa/itoa.go b/internal/stdlib/itoa/itoa.go new file mode 100644 index 0000000000..c6062d9fe1 --- /dev/null +++ b/internal/stdlib/itoa/itoa.go @@ -0,0 +1,33 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Simple conversions to avoid depending on strconv. + +package itoa + +// Itoa converts val to a decimal string. +func Itoa(val int) string { + if val < 0 { + return "-" + Uitoa(uint(-val)) + } + return Uitoa(uint(val)) +} + +// Uitoa converts val to a decimal string. +func Uitoa(val uint) string { + if val == 0 { // avoid string allocation + return "0" + } + var buf [20]byte // big enough for 64bit value base 10 + i := len(buf) - 1 + for val >= 10 { + q := val / 10 + buf[i] = byte('0' + val - q*10) + i-- + val = q + } + // val < 10 + buf[i] = byte('0' + val) + return string(buf[i:]) +} diff --git a/internal/stdlib/os/exec.go b/internal/stdlib/os/exec.go new file mode 100644 index 0000000000..bc75d4dd66 --- /dev/null +++ b/internal/stdlib/os/exec.go @@ -0,0 +1,176 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package os + +import ( + "errors" + "internal/testlog" + "runtime" + "sync" + "sync/atomic" + "syscall" + "time" +) + +// ErrProcessDone indicates a Process has finished. +var ErrProcessDone = errors.New("os: process already finished") + +// Process stores the information about a process created by StartProcess. +type Process struct { + Pid int + handle uintptr // handle is accessed atomically on Windows + isdone uint32 // process has been successfully waited on, non zero if true + sigMu sync.RWMutex // avoid race between wait and signal +} + +func newProcess(pid int, handle uintptr) *Process { + p := &Process{Pid: pid, handle: handle} + runtime.SetFinalizer(p, (*Process).Release) + return p +} + +func (p *Process) setDone() { + atomic.StoreUint32(&p.isdone, 1) +} + +func (p *Process) done() bool { + return atomic.LoadUint32(&p.isdone) > 0 +} + +// ProcAttr holds the attributes that will be applied to a new process +// started by StartProcess. +type ProcAttr struct { + // If Dir is non-empty, the child changes into the directory before + // creating the process. + Dir string + // If Env is non-nil, it gives the environment variables for the + // new process in the form returned by Environ. + // If it is nil, the result of Environ will be used. + Env []string + // Files specifies the open files inherited by the new process. The + // first three entries correspond to standard input, standard output, and + // standard error. An implementation may support additional entries, + // depending on the underlying operating system. A nil entry corresponds + // to that file being closed when the process starts. + // On Unix systems, StartProcess will change these File values + // to blocking mode, which means that SetDeadline will stop working + // and calling Close will not interrupt a Read or Write. + Files []*File + + // Operating system-specific process creation attributes. + // Note that setting this field means that your program + // may not execute properly or even compile on some + // operating systems. + Sys *syscall.SysProcAttr +} + +// A Signal represents an operating system signal. +// The usual underlying implementation is operating system-dependent: +// on Unix it is syscall.Signal. +type Signal interface { + String() string + Signal() // to distinguish from other Stringers +} + +// Getpid returns the process id of the caller. +func Getpid() int { return syscall.Getpid() } + +// Getppid returns the process id of the caller's parent. +func Getppid() int { return syscall.Getppid() } + +// FindProcess looks for a running process by its pid. +// +// The Process it returns can be used to obtain information +// about the underlying operating system process. +// +// On Unix systems, FindProcess always succeeds and returns a Process +// for the given pid, regardless of whether the process exists. +func FindProcess(pid int) (*Process, error) { + return findProcess(pid) +} + +// StartProcess starts a new process with the program, arguments and attributes +// specified by name, argv and attr. The argv slice will become os.Args in the +// new process, so it normally starts with the program name. +// +// If the calling goroutine has locked the operating system thread +// with runtime.LockOSThread and modified any inheritable OS-level +// thread state (for example, Linux or Plan 9 name spaces), the new +// process will inherit the caller's thread state. +// +// StartProcess is a low-level interface. The os/exec package provides +// higher-level interfaces. +// +// If there is an error, it will be of type *PathError. +func StartProcess(name string, argv []string, attr *ProcAttr) (*Process, error) { + testlog.Open(name) + return startProcess(name, argv, attr) +} + +// Release releases any resources associated with the Process p, +// rendering it unusable in the future. +// Release only needs to be called if Wait is not. +func (p *Process) Release() error { + return p.release() +} + +// Kill causes the Process to exit immediately. Kill does not wait until +// the Process has actually exited. This only kills the Process itself, +// not any other processes it may have started. +func (p *Process) Kill() error { + return p.kill() +} + +// Wait waits for the Process to exit, and then returns a +// ProcessState describing its status and an error, if any. +// Wait releases any resources associated with the Process. +// On most operating systems, the Process must be a child +// of the current process or an error will be returned. +func (p *Process) Wait() (*ProcessState, error) { + return p.wait() +} + +// Signal sends a signal to the Process. +// Sending Interrupt on Windows is not implemented. +func (p *Process) Signal(sig Signal) error { + return p.signal(sig) +} + +// UserTime returns the user CPU time of the exited process and its children. +func (p *ProcessState) UserTime() time.Duration { + return p.userTime() +} + +// SystemTime returns the system CPU time of the exited process and its children. +func (p *ProcessState) SystemTime() time.Duration { + return p.systemTime() +} + +// Exited reports whether the program has exited. +func (p *ProcessState) Exited() bool { + return p.exited() +} + +// Success reports whether the program exited successfully, +// such as with exit status 0 on Unix. +func (p *ProcessState) Success() bool { + return p.success() +} + +// Sys returns system-dependent exit information about +// the process. Convert it to the appropriate underlying +// type, such as syscall.WaitStatus on Unix, to access its contents. +func (p *ProcessState) Sys() interface{} { + return p.sys() +} + +// SysUsage returns system-dependent resource usage information about +// the exited process. Convert it to the appropriate underlying +// type, such as *syscall.Rusage on Unix, to access its contents. +// (On Unix, *syscall.Rusage matches struct rusage as defined in the +// getrusage(2) manual page.) +func (p *ProcessState) SysUsage() interface{} { + return p.sysUsage() +} diff --git a/internal/stdlib/os/exec/exec.go b/internal/stdlib/os/exec/exec.go new file mode 100644 index 0000000000..bc9fd5aa96 --- /dev/null +++ b/internal/stdlib/os/exec/exec.go @@ -0,0 +1,789 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package exec runs external commands. It wraps os.StartProcess to make it +// easier to remap stdin and stdout, connect I/O with pipes, and do other +// adjustments. +// +// Unlike the "system" library call from C and other languages, the +// os/exec package intentionally does not invoke the system shell and +// does not expand any glob patterns or handle other expansions, +// pipelines, or redirections typically done by shells. The package +// behaves more like C's "exec" family of functions. To expand glob +// patterns, either call the shell directly, taking care to escape any +// dangerous input, or use the path/filepath package's Glob function. +// To expand environment variables, use package os's ExpandEnv. +// +// Note that the examples in this package assume a Unix system. +// They may not run on Windows, and they do not run in the Go Playground +// used by golang.org and godoc.org. +package exec + +import ( + "bytes" + "context" + "errors" + "internal/syscall/execenv + "io" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "sync" + "syscall" +) + +// Error is returned by LookPath when it fails to classify a file as an +// executable. +type Error struct { + // Name is the file name for which the error occurred. + Name string + // Err is the underlying error. + Err error +} + +func (e *Error) Error() string { + return "exec: " + strconv.Quote(e.Name) + ": " + e.Err.Error() +} + +func (e *Error) Unwrap() error { return e.Err } + +// Cmd represents an external command being prepared or run. +// +// A Cmd cannot be reused after calling its Run, Output or CombinedOutput +// methods. +type Cmd struct { + // Path is the path of the command to run. + // + // This is the only field that must be set to a non-zero + // value. If Path is relative, it is evaluated relative + // to Dir. + Path string + + // Args holds command line arguments, including the command as Args[0]. + // If the Args field is empty or nil, Run uses {Path}. + // + // In typical use, both Path and Args are set by calling Command. + Args []string + + // Env specifies the environment of the process. + // Each entry is of the form "key=value". + // If Env is nil, the new process uses the current process's + // environment. + // If Env contains duplicate environment keys, only the last + // value in the slice for each duplicate key is used. + // As a special case on Windows, SYSTEMROOT is always added if + // missing and not explicitly set to the empty string. + Env []string + + // Dir specifies the working directory of the command. + // If Dir is the empty string, Run runs the command in the + // calling process's current directory. + Dir string + + // Stdin specifies the process's standard input. + // + // If Stdin is nil, the process reads from the null device (os.DevNull). + // + // If Stdin is an *os.File, the process's standard input is connected + // directly to that file. + // + // Otherwise, during the execution of the command a separate + // goroutine reads from Stdin and delivers that data to the command + // over a pipe. In this case, Wait does not complete until the goroutine + // stops copying, either because it has reached the end of Stdin + // (EOF or a read error) or because writing to the pipe returned an error. + Stdin io.Reader + + // Stdout and Stderr specify the process's standard output and error. + // + // If either is nil, Run connects the corresponding file descriptor + // to the null device (os.DevNull). + // + // If either is an *os.File, the corresponding output from the process + // is connected directly to that file. + // + // Otherwise, during the execution of the command a separate goroutine + // reads from the process over a pipe and delivers that data to the + // corresponding Writer. In this case, Wait does not complete until the + // goroutine reaches EOF or encounters an error. + // + // If Stdout and Stderr are the same writer, and have a type that can + // be compared with ==, at most one goroutine at a time will call Write. + Stdout io.Writer + Stderr io.Writer + + // ExtraFiles specifies additional open files to be inherited by the + // new process. It does not include standard input, standard output, or + // standard error. If non-nil, entry i becomes file descriptor 3+i. + // + // ExtraFiles is not supported on Windows. + ExtraFiles []*os.File + + // SysProcAttr holds optional, operating system-specific attributes. + // Run passes it to os.StartProcess as the os.ProcAttr's Sys field. + SysProcAttr *syscall.SysProcAttr + + // Process is the underlying process, once started. + Process *os.Process + + // ProcessState contains information about an exited process, + // available after a call to Wait or Run. + ProcessState *os.ProcessState + + ctx context.Context // nil means none + lookPathErr error // LookPath error, if any. + finished bool // when Wait was called + childFiles []*os.File + closeAfterStart []io.Closer + closeAfterWait []io.Closer + goroutine []func() error + errch chan error // one send per goroutine + waitDone chan struct{} +} + +// Command returns the Cmd struct to execute the named program with +// the given arguments. +// +// It sets only the Path and Args in the returned structure. +// +// If name contains no path separators, Command uses LookPath to +// resolve name to a complete path if possible. Otherwise it uses name +// directly as Path. +// +// The returned Cmd's Args field is constructed from the command name +// followed by the elements of arg, so arg should not include the +// command name itself. For example, Command("echo", "hello"). +// Args[0] is always name, not the possibly resolved Path. +// +// On Windows, processes receive the whole command line as a single string +// and do their own parsing. Command combines and quotes Args into a command +// line string with an algorithm compatible with applications using +// CommandLineToArgvW (which is the most common way). Notable exceptions are +// msiexec.exe and cmd.exe (and thus, all batch files), which have a different +// unquoting algorithm. In these or other similar cases, you can do the +// quoting yourself and provide the full command line in SysProcAttr.CmdLine, +// leaving Args empty. +func Command(name string, arg ...string) *Cmd { + cmd := &Cmd{ + Path: name, + Args: append([]string{name}, arg...), + } + if filepath.Base(name) == name { + if lp, err := LookPath(name); err != nil { + cmd.lookPathErr = err + } else { + cmd.Path = lp + } + } + return cmd +} + +// CommandContext is like Command but includes a context. +// +// The provided context is used to kill the process (by calling +// os.Process.Kill) if the context becomes done before the command +// completes on its own. +func CommandContext(ctx context.Context, name string, arg ...string) *Cmd { + if ctx == nil { + panic("nil Context") + } + cmd := Command(name, arg...) + cmd.ctx = ctx + return cmd +} + +// String returns a human-readable description of c. +// It is intended only for debugging. +// In particular, it is not suitable for use as input to a shell. +// The output of String may vary across Go releases. +func (c *Cmd) String() string { + if c.lookPathErr != nil { + // failed to resolve path; report the original requested path (plus args) + return strings.Join(c.Args, " ") + } + // report the exact executable path (plus args) + b := new(strings.Builder) + b.WriteString(c.Path) + for _, a := range c.Args[1:] { + b.WriteByte(' ') + b.WriteString(a) + } + return b.String() +} + +// interfaceEqual protects against panics from doing equality tests on +// two interfaces with non-comparable underlying types. +func interfaceEqual(a, b interface{}) bool { + defer func() { + recover() + }() + return a == b +} + +func (c *Cmd) envv() ([]string, error) { + if c.Env != nil { + return c.Env, nil + } + return execenv.Default(c.SysProcAttr) +} + +func (c *Cmd) argv() []string { + if len(c.Args) > 0 { + return c.Args + } + return []string{c.Path} +} + +// skipStdinCopyError optionally specifies a function which reports +// whether the provided stdin copy error should be ignored. +var skipStdinCopyError func(error) bool + +func (c *Cmd) stdin() (f *os.File, err error) { + if c.Stdin == nil { + f, err = os.Open(os.DevNull) + if err != nil { + return + } + c.closeAfterStart = append(c.closeAfterStart, f) + return + } + + if f, ok := c.Stdin.(*os.File); ok { + return f, nil + } + + pr, pw, err := os.Pipe() + if err != nil { + return + } + + c.closeAfterStart = append(c.closeAfterStart, pr) + c.closeAfterWait = append(c.closeAfterWait, pw) + c.goroutine = append(c.goroutine, func() error { + _, err := io.Copy(pw, c.Stdin) + if skip := skipStdinCopyError; skip != nil && skip(err) { + err = nil + } + if err1 := pw.Close(); err == nil { + err = err1 + } + return err + }) + return pr, nil +} + +func (c *Cmd) stdout() (f *os.File, err error) { + return c.writerDescriptor(c.Stdout) +} + +func (c *Cmd) stderr() (f *os.File, err error) { + if c.Stderr != nil && interfaceEqual(c.Stderr, c.Stdout) { + return c.childFiles[1], nil + } + return c.writerDescriptor(c.Stderr) +} + +func (c *Cmd) writerDescriptor(w io.Writer) (f *os.File, err error) { + if w == nil { + f, err = os.OpenFile(os.DevNull, os.O_WRONLY, 0) + if err != nil { + return + } + c.closeAfterStart = append(c.closeAfterStart, f) + return + } + + if f, ok := w.(*os.File); ok { + return f, nil + } + + pr, pw, err := os.Pipe() + if err != nil { + return + } + + c.closeAfterStart = append(c.closeAfterStart, pw) + c.closeAfterWait = append(c.closeAfterWait, pr) + c.goroutine = append(c.goroutine, func() error { + _, err := io.Copy(w, pr) + pr.Close() // in case io.Copy stopped due to write error + return err + }) + return pw, nil +} + +func (c *Cmd) closeDescriptors(closers []io.Closer) { + for _, fd := range closers { + fd.Close() + } +} + +// Run starts the specified command and waits for it to complete. +// +// The returned error is nil if the command runs, has no problems +// copying stdin, stdout, and stderr, and exits with a zero exit +// status. +// +// If the command starts but does not complete successfully, the error is of +// type *ExitError. Other error types may be returned for other situations. +// +// If the calling goroutine has locked the operating system thread +// with runtime.LockOSThread and modified any inheritable OS-level +// thread state (for example, Linux or Plan 9 name spaces), the new +// process will inherit the caller's thread state. +func (c *Cmd) Run() error { + if err := c.Start(); err != nil { + return err + } + return c.Wait() +} + +// lookExtensions finds windows executable by its dir and path. +// It uses LookPath to try appropriate extensions. +// lookExtensions does not search PATH, instead it converts `prog` into `.\prog`. +func lookExtensions(path, dir string) (string, error) { + if filepath.Base(path) == path { + path = filepath.Join(".", path) + } + if dir == "" { + return LookPath(path) + } + if filepath.VolumeName(path) != "" { + return LookPath(path) + } + if len(path) > 1 && os.IsPathSeparator(path[0]) { + return LookPath(path) + } + dirandpath := filepath.Join(dir, path) + // We assume that LookPath will only add file extension. + lp, err := LookPath(dirandpath) + if err != nil { + return "", err + } + ext := strings.TrimPrefix(lp, dirandpath) + return path + ext, nil +} + +// Start starts the specified command but does not wait for it to complete. +// +// If Start returns successfully, the c.Process field will be set. +// +// The Wait method will return the exit code and release associated resources +// once the command exits. +func (c *Cmd) Start() error { + if c.lookPathErr != nil { + c.closeDescriptors(c.closeAfterStart) + c.closeDescriptors(c.closeAfterWait) + return c.lookPathErr + } + if runtime.GOOS == "windows" { + lp, err := lookExtensions(c.Path, c.Dir) + if err != nil { + c.closeDescriptors(c.closeAfterStart) + c.closeDescriptors(c.closeAfterWait) + return err + } + c.Path = lp + } + if c.Process != nil { + return errors.New("exec: already started") + } + if c.ctx != nil { + select { + case <-c.ctx.Done(): + c.closeDescriptors(c.closeAfterStart) + c.closeDescriptors(c.closeAfterWait) + return c.ctx.Err() + default: + } + } + + c.childFiles = make([]*os.File, 0, 3+len(c.ExtraFiles)) + type F func(*Cmd) (*os.File, error) + for _, setupFd := range []F{(*Cmd).stdin, (*Cmd).stdout, (*Cmd).stderr} { + fd, err := setupFd(c) + if err != nil { + c.closeDescriptors(c.closeAfterStart) + c.closeDescriptors(c.closeAfterWait) + return err + } + c.childFiles = append(c.childFiles, fd) + } + c.childFiles = append(c.childFiles, c.ExtraFiles...) + + envv, err := c.envv() + if err != nil { + return err + } + + c.Process, err = os.StartProcess(c.Path, c.argv(), &os.ProcAttr{ + Dir: c.Dir, + Files: c.childFiles, + Env: addCriticalEnv(dedupEnv(envv)), + Sys: c.SysProcAttr, + }) + if err != nil { + c.closeDescriptors(c.closeAfterStart) + c.closeDescriptors(c.closeAfterWait) + return err + } + + c.closeDescriptors(c.closeAfterStart) + + // Don't allocate the channel unless there are goroutines to fire. + if len(c.goroutine) > 0 { + c.errch = make(chan error, len(c.goroutine)) + for _, fn := range c.goroutine { + go func(fn func() error) { + c.errch <- fn() + }(fn) + } + } + + if c.ctx != nil { + c.waitDone = make(chan struct{}) + go func() { + select { + case <-c.ctx.Done(): + c.Process.Kill() + case <-c.waitDone: + } + }() + } + + return nil +} + +// An ExitError reports an unsuccessful exit by a command. +type ExitError struct { + *os.ProcessState + + // Stderr holds a subset of the standard error output from the + // Cmd.Output method if standard error was not otherwise being + // collected. + // + // If the error output is long, Stderr may contain only a prefix + // and suffix of the output, with the middle replaced with + // text about the number of omitted bytes. + // + // Stderr is provided for debugging, for inclusion in error messages. + // Users with other needs should redirect Cmd.Stderr as needed. + Stderr []byte +} + +func (e *ExitError) Error() string { + return e.ProcessState.String() +} + +// Wait waits for the command to exit and waits for any copying to +// stdin or copying from stdout or stderr to complete. +// +// The command must have been started by Start. +// +// The returned error is nil if the command runs, has no problems +// copying stdin, stdout, and stderr, and exits with a zero exit +// status. +// +// If the command fails to run or doesn't complete successfully, the +// error is of type *ExitError. Other error types may be +// returned for I/O problems. +// +// If any of c.Stdin, c.Stdout or c.Stderr are not an *os.File, Wait also waits +// for the respective I/O loop copying to or from the process to complete. +// +// Wait releases any resources associated with the Cmd. +func (c *Cmd) Wait() error { + if c.Process == nil { + return errors.New("exec: not started") + } + if c.finished { + return errors.New("exec: Wait was already called") + } + c.finished = true + + state, err := c.Process.Wait() + if c.waitDone != nil { + close(c.waitDone) + } + c.ProcessState = state + + var copyError error + for range c.goroutine { + if err := <-c.errch; err != nil && copyError == nil { + copyError = err + } + } + + c.closeDescriptors(c.closeAfterWait) + + if err != nil { + return err + } else if !state.Success() { + return &ExitError{ProcessState: state} + } + + return copyError +} + +// Output runs the command and returns its standard output. +// Any returned error will usually be of type *ExitError. +// If c.Stderr was nil, Output populates ExitError.Stderr. +func (c *Cmd) Output() ([]byte, error) { + if c.Stdout != nil { + return nil, errors.New("exec: Stdout already set") + } + var stdout bytes.Buffer + c.Stdout = &stdout + + captureErr := c.Stderr == nil + if captureErr { + c.Stderr = &prefixSuffixSaver{N: 32 << 10} + } + + err := c.Run() + if err != nil && captureErr { + if ee, ok := err.(*ExitError); ok { + ee.Stderr = c.Stderr.(*prefixSuffixSaver).Bytes() + } + } + return stdout.Bytes(), err +} + +// CombinedOutput runs the command and returns its combined standard +// output and standard error. +func (c *Cmd) CombinedOutput() ([]byte, error) { + if c.Stdout != nil { + return nil, errors.New("exec: Stdout already set") + } + if c.Stderr != nil { + return nil, errors.New("exec: Stderr already set") + } + var b bytes.Buffer + c.Stdout = &b + c.Stderr = &b + err := c.Run() + return b.Bytes(), err +} + +// StdinPipe returns a pipe that will be connected to the command's +// standard input when the command starts. +// The pipe will be closed automatically after Wait sees the command exit. +// A caller need only call Close to force the pipe to close sooner. +// For example, if the command being run will not exit until standard input +// is closed, the caller must close the pipe. +func (c *Cmd) StdinPipe() (io.WriteCloser, error) { + if c.Stdin != nil { + return nil, errors.New("exec: Stdin already set") + } + if c.Process != nil { + return nil, errors.New("exec: StdinPipe after process started") + } + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + c.Stdin = pr + c.closeAfterStart = append(c.closeAfterStart, pr) + wc := &closeOnce{File: pw} + c.closeAfterWait = append(c.closeAfterWait, wc) + return wc, nil +} + +type closeOnce struct { + *os.File + + once sync.Once + err error +} + +func (c *closeOnce) Close() error { + c.once.Do(c.close) + return c.err +} + +func (c *closeOnce) close() { + c.err = c.File.Close() +} + +// StdoutPipe returns a pipe that will be connected to the command's +// standard output when the command starts. +// +// Wait will close the pipe after seeing the command exit, so most callers +// need not close the pipe themselves. It is thus incorrect to call Wait +// before all reads from the pipe have completed. +// For the same reason, it is incorrect to call Run when using StdoutPipe. +// See the example for idiomatic usage. +func (c *Cmd) StdoutPipe() (io.ReadCloser, error) { + if c.Stdout != nil { + return nil, errors.New("exec: Stdout already set") + } + if c.Process != nil { + return nil, errors.New("exec: StdoutPipe after process started") + } + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + c.Stdout = pw + c.closeAfterStart = append(c.closeAfterStart, pw) + c.closeAfterWait = append(c.closeAfterWait, pr) + return pr, nil +} + +// StderrPipe returns a pipe that will be connected to the command's +// standard error when the command starts. +// +// Wait will close the pipe after seeing the command exit, so most callers +// need not close the pipe themselves. It is thus incorrect to call Wait +// before all reads from the pipe have completed. +// For the same reason, it is incorrect to use Run when using StderrPipe. +// See the StdoutPipe example for idiomatic usage. +func (c *Cmd) StderrPipe() (io.ReadCloser, error) { + if c.Stderr != nil { + return nil, errors.New("exec: Stderr already set") + } + if c.Process != nil { + return nil, errors.New("exec: StderrPipe after process started") + } + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + c.Stderr = pw + c.closeAfterStart = append(c.closeAfterStart, pw) + c.closeAfterWait = append(c.closeAfterWait, pr) + return pr, nil +} + +// prefixSuffixSaver is an io.Writer which retains the first N bytes +// and the last N bytes written to it. The Bytes() methods reconstructs +// it with a pretty error message. +type prefixSuffixSaver struct { + N int // max size of prefix or suffix + prefix []byte + suffix []byte // ring buffer once len(suffix) == N + suffixOff int // offset to write into suffix + skipped int64 + + // TODO(bradfitz): we could keep one large []byte and use part of it for + // the prefix, reserve space for the '... Omitting N bytes ...' message, + // then the ring buffer suffix, and just rearrange the ring buffer + // suffix when Bytes() is called, but it doesn't seem worth it for + // now just for error messages. It's only ~64KB anyway. +} + +func (w *prefixSuffixSaver) Write(p []byte) (n int, err error) { + lenp := len(p) + p = w.fill(&w.prefix, p) + + // Only keep the last w.N bytes of suffix data. + if overage := len(p) - w.N; overage > 0 { + p = p[overage:] + w.skipped += int64(overage) + } + p = w.fill(&w.suffix, p) + + // w.suffix is full now if p is non-empty. Overwrite it in a circle. + for len(p) > 0 { // 0, 1, or 2 iterations. + n := copy(w.suffix[w.suffixOff:], p) + p = p[n:] + w.skipped += int64(n) + w.suffixOff += n + if w.suffixOff == w.N { + w.suffixOff = 0 + } + } + return lenp, nil +} + +// fill appends up to len(p) bytes of p to *dst, such that *dst does not +// grow larger than w.N. It returns the un-appended suffix of p. +func (w *prefixSuffixSaver) fill(dst *[]byte, p []byte) (pRemain []byte) { + if remain := w.N - len(*dst); remain > 0 { + add := minInt(len(p), remain) + *dst = append(*dst, p[:add]...) + p = p[add:] + } + return p +} + +func (w *prefixSuffixSaver) Bytes() []byte { + if w.suffix == nil { + return w.prefix + } + if w.skipped == 0 { + return append(w.prefix, w.suffix...) + } + var buf bytes.Buffer + buf.Grow(len(w.prefix) + len(w.suffix) + 50) + buf.Write(w.prefix) + buf.WriteString("\n... omitting ") + buf.WriteString(strconv.FormatInt(w.skipped, 10)) + buf.WriteString(" bytes ...\n") + buf.Write(w.suffix[w.suffixOff:]) + buf.Write(w.suffix[:w.suffixOff]) + return buf.Bytes() +} + +func minInt(a, b int) int { + if a < b { + return a + } + return b +} + +// dedupEnv returns a copy of env with any duplicates removed, in favor of +// later values. +// Items not of the normal environment "key=value" form are preserved unchanged. +func dedupEnv(env []string) []string { + return dedupEnvCase(runtime.GOOS == "windows", env) +} + +// dedupEnvCase is dedupEnv with a case option for testing. +// If caseInsensitive is true, the case of keys is ignored. +func dedupEnvCase(caseInsensitive bool, env []string) []string { + out := make([]string, 0, len(env)) + saw := make(map[string]int, len(env)) // key => index into out + for _, kv := range env { + eq := strings.Index(kv, "=") + if eq < 0 { + out = append(out, kv) + continue + } + k := kv[:eq] + if caseInsensitive { + k = strings.ToLower(k) + } + if dupIdx, isDup := saw[k]; isDup { + out[dupIdx] = kv + continue + } + saw[k] = len(out) + out = append(out, kv) + } + return out +} + +// addCriticalEnv adds any critical environment variables that are required +// (or at least almost always required) on the operating system. +// Currently this is only used for Windows. +func addCriticalEnv(env []string) []string { + if runtime.GOOS != "windows" { + return env + } + for _, kv := range env { + eq := strings.Index(kv, "=") + if eq < 0 { + continue + } + k := kv[:eq] + if strings.EqualFold(k, "SYSTEMROOT") { + // We already have it. + return env + } + } + return append(env, "SYSTEMROOT="+os.Getenv("SYSTEMROOT")) +} diff --git a/internal/stdlib/os/exec/exec_windows.go b/internal/stdlib/os/exec/exec_windows.go new file mode 100644 index 0000000000..bb937f8aed --- /dev/null +++ b/internal/stdlib/os/exec/exec_windows.go @@ -0,0 +1,23 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package exec + +import ( + "io/fs" + "syscall" +) + +func init() { + skipStdinCopyError = func(err error) bool { + // Ignore ERROR_BROKEN_PIPE and ERROR_NO_DATA errors copying + // to stdin if the program completed successfully otherwise. + // See Issue 20445. + const _ERROR_NO_DATA = syscall.Errno(0xe8) + pe, ok := err.(*fs.PathError) + return ok && + pe.Op == "write" && pe.Path == "|1" && + (pe.Err == syscall.ERROR_BROKEN_PIPE || pe.Err == _ERROR_NO_DATA) + } +} diff --git a/internal/stdlib/os/exec/lp_windows.go b/internal/stdlib/os/exec/lp_windows.go new file mode 100644 index 0000000000..e7a2cdf142 --- /dev/null +++ b/internal/stdlib/os/exec/lp_windows.go @@ -0,0 +1,94 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package exec + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + "strings" +) + +// ErrNotFound is the error resulting if a path search failed to find an executable file. +var ErrNotFound = errors.New("executable file not found in %PATH%") + +func chkStat(file string) error { + d, err := os.Stat(file) + if err != nil { + return err + } + if d.IsDir() { + return fs.ErrPermission + } + return nil +} + +func hasExt(file string) bool { + i := strings.LastIndex(file, ".") + if i < 0 { + return false + } + return strings.LastIndexAny(file, `:\/`) < i +} + +func findExecutable(file string, exts []string) (string, error) { + if len(exts) == 0 { + return file, chkStat(file) + } + if hasExt(file) { + if chkStat(file) == nil { + return file, nil + } + } + for _, e := range exts { + if f := file + e; chkStat(f) == nil { + return f, nil + } + } + return "", fs.ErrNotExist +} + +// LookPath searches for an executable named file in the +// directories named by the PATH environment variable. +// If file contains a slash, it is tried directly and the PATH is not consulted. +// LookPath also uses PATHEXT environment variable to match +// a suitable candidate. +// The result may be an absolute path or a path relative to the current directory. +func LookPath(file string) (string, error) { + var exts []string + x := os.Getenv(`PATHEXT`) + if x != "" { + for _, e := range strings.Split(strings.ToLower(x), `;`) { + if e == "" { + continue + } + if e[0] != '.' { + e = "." + e + } + exts = append(exts, e) + } + } else { + exts = []string{".com", ".exe", ".bat", ".cmd"} + } + + if strings.ContainsAny(file, `:\/`) { + if f, err := findExecutable(file, exts); err == nil { + return f, nil + } else { + return "", &Error{file, err} + } + } + if f, err := findExecutable(filepath.Join(".", file), exts); err == nil { + return f, nil + } + path := os.Getenv("path") + for _, dir := range filepath.SplitList(path) { + if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil { + return f, nil + } + } + return "", &Error{file, ErrNotFound} +} diff --git a/internal/stdlib/os/exec_posix.go b/internal/stdlib/os/exec_posix.go new file mode 100644 index 0000000000..e8736f7c54 --- /dev/null +++ b/internal/stdlib/os/exec_posix.go @@ -0,0 +1,137 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build aix || darwin || dragonfly || freebsd || (js && wasm) || linux || netbsd || openbsd || solaris || windows +// +build aix darwin dragonfly freebsd js,wasm linux netbsd openbsd solaris windows + +package os + +import ( + "internal/itoa" + "internal/syscall/execenv" + "runtime" + "syscall" +) + +// The only signal values guaranteed to be present in the os package on all +// systems are os.Interrupt (send the process an interrupt) and os.Kill (force +// the process to exit). On Windows, sending os.Interrupt to a process with +// os.Process.Signal is not implemented; it will return an error instead of +// sending a signal. +var ( + Interrupt Signal = syscall.SIGINT + Kill Signal = syscall.SIGKILL +) + +func startProcess(name string, argv []string, attr *ProcAttr) (p *Process, err error) { + // If there is no SysProcAttr (ie. no Chroot or changed + // UID/GID), double-check existence of the directory we want + // to chdir into. We can make the error clearer this way. + if attr != nil && attr.Sys == nil && attr.Dir != "" { + if _, err := Stat(attr.Dir); err != nil { + pe := err.(*PathError) + pe.Op = "chdir" + return nil, pe + } + } + + sysattr := &syscall.ProcAttr{ + Dir: attr.Dir, + Env: attr.Env, + Sys: attr.Sys, + } + if sysattr.Env == nil { + sysattr.Env, err = execenv.Default(sysattr.Sys) + if err != nil { + return nil, err + } + } + sysattr.Files = make([]uintptr, 0, len(attr.Files)) + for _, f := range attr.Files { + sysattr.Files = append(sysattr.Files, f.Fd()) + } + + pid, h, e := syscall.StartProcess(name, argv, sysattr) + + // Make sure we don't run the finalizers of attr.Files. + runtime.KeepAlive(attr) + + if e != nil { + return nil, &PathError{Op: "fork/exec", Path: name, Err: e} + } + + return newProcess(pid, h), nil +} + +func (p *Process) kill() error { + return p.Signal(Kill) +} + +// ProcessState stores information about a process, as reported by Wait. +type ProcessState struct { + pid int // The process's id. + status syscall.WaitStatus // System-dependent status info. + rusage *syscall.Rusage +} + +// Pid returns the process id of the exited process. +func (p *ProcessState) Pid() int { + return p.pid +} + +func (p *ProcessState) exited() bool { + return p.status.Exited() +} + +func (p *ProcessState) success() bool { + return p.status.ExitStatus() == 0 +} + +func (p *ProcessState) sys() interface{} { + return p.status +} + +func (p *ProcessState) sysUsage() interface{} { + return p.rusage +} + +func (p *ProcessState) String() string { + if p == nil { + return "" + } + status := p.Sys().(syscall.WaitStatus) + res := "" + switch { + case status.Exited(): + code := status.ExitStatus() + if runtime.GOOS == "windows" && uint(code) >= 1<<16 { // windows uses large hex numbers + res = "exit status " + uitox(uint(code)) + } else { // unix systems use small decimal integers + res = "exit status " + itoa.Itoa(code) // unix + } + case status.Signaled(): + res = "signal: " + status.Signal().String() + case status.Stopped(): + res = "stop signal: " + status.StopSignal().String() + if status.StopSignal() == syscall.SIGTRAP && status.TrapCause() != 0 { + res += " (trap " + itoa.Itoa(status.TrapCause()) + ")" + } + case status.Continued(): + res = "continued" + } + if status.CoreDump() { + res += " (core dumped)" + } + return res +} + +// ExitCode returns the exit code of the exited process, or -1 +// if the process hasn't exited or was terminated by a signal. +func (p *ProcessState) ExitCode() int { + // return -1 if the process hasn't started. + if p == nil { + return -1 + } + return p.status.ExitStatus() +} diff --git a/internal/stdlib/os/exec_windows.go b/internal/stdlib/os/exec_windows.go new file mode 100644 index 0000000000..239bed198f --- /dev/null +++ b/internal/stdlib/os/exec_windows.go @@ -0,0 +1,181 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package os + +import ( + "errors" + "internal/syscall/windows" + "runtime" + "sync/atomic" + "syscall" + "time" +) + +func (p *Process) wait() (ps *ProcessState, err error) { + handle := atomic.LoadUintptr(&p.handle) + s, e := syscall.WaitForSingleObject(syscall.Handle(handle), syscall.INFINITE) + switch s { + case syscall.WAIT_OBJECT_0: + break + case syscall.WAIT_FAILED: + return nil, NewSyscallError("WaitForSingleObject", e) + default: + return nil, errors.New("os: unexpected result from WaitForSingleObject") + } + var ec uint32 + e = syscall.GetExitCodeProcess(syscall.Handle(handle), &ec) + if e != nil { + return nil, NewSyscallError("GetExitCodeProcess", e) + } + var u syscall.Rusage + e = syscall.GetProcessTimes(syscall.Handle(handle), &u.CreationTime, &u.ExitTime, &u.KernelTime, &u.UserTime) + if e != nil { + return nil, NewSyscallError("GetProcessTimes", e) + } + p.setDone() + // NOTE(brainman): It seems that sometimes process is not dead + // when WaitForSingleObject returns. But we do not know any + // other way to wait for it. Sleeping for a while seems to do + // the trick sometimes. + // See https://golang.org/issue/25965 for details. + defer time.Sleep(5 * time.Millisecond) + defer p.Release() + return &ProcessState{p.Pid, syscall.WaitStatus{ExitCode: ec}, &u}, nil +} + +func (p *Process) signal(sig Signal) error { + handle := atomic.LoadUintptr(&p.handle) + if handle == uintptr(syscall.InvalidHandle) { + return syscall.EINVAL + } + if p.done() { + return ErrProcessDone + } + if sig == Kill { + var terminationHandle syscall.Handle + e := syscall.DuplicateHandle(^syscall.Handle(0), syscall.Handle(handle), ^syscall.Handle(0), &terminationHandle, syscall.PROCESS_TERMINATE, false, 0) + if e != nil { + return NewSyscallError("DuplicateHandle", e) + } + runtime.KeepAlive(p) + defer syscall.CloseHandle(terminationHandle) + e = syscall.TerminateProcess(syscall.Handle(terminationHandle), 1) + return NewSyscallError("TerminateProcess", e) + } + // TODO(rsc): Handle Interrupt too? + return syscall.Errno(syscall.EWINDOWS) +} + +func (p *Process) release() error { + handle := atomic.SwapUintptr(&p.handle, uintptr(syscall.InvalidHandle)) + if handle == uintptr(syscall.InvalidHandle) { + return syscall.EINVAL + } + e := syscall.CloseHandle(syscall.Handle(handle)) + if e != nil { + return NewSyscallError("CloseHandle", e) + } + // no need for a finalizer anymore + runtime.SetFinalizer(p, nil) + return nil +} + +func findProcess(pid int) (p *Process, err error) { + const da = syscall.STANDARD_RIGHTS_READ | + syscall.PROCESS_QUERY_INFORMATION | syscall.SYNCHRONIZE + h, e := syscall.OpenProcess(da, false, uint32(pid)) + if e != nil { + return nil, NewSyscallError("OpenProcess", e) + } + return newProcess(pid, uintptr(h)), nil +} + +func init() { + cmd := windows.UTF16PtrToString(syscall.GetCommandLine()) + if len(cmd) == 0 { + arg0, _ := Executable() + Args = []string{arg0} + } else { + Args = commandLineToArgv(cmd) + } +} + +// appendBSBytes appends n '\\' bytes to b and returns the resulting slice. +func appendBSBytes(b []byte, n int) []byte { + for ; n > 0; n-- { + b = append(b, '\\') + } + return b +} + +// readNextArg splits command line string cmd into next +// argument and command line remainder. +func readNextArg(cmd string) (arg []byte, rest string) { + var b []byte + var inquote bool + var nslash int + for ; len(cmd) > 0; cmd = cmd[1:] { + c := cmd[0] + switch c { + case ' ', '\t': + if !inquote { + return appendBSBytes(b, nslash), cmd[1:] + } + case '"': + b = appendBSBytes(b, nslash/2) + if nslash%2 == 0 { + // use "Prior to 2008" rule from + // http://daviddeley.com/autohotkey/parameters/parameters.htm + // section 5.2 to deal with double double quotes + if inquote && len(cmd) > 1 && cmd[1] == '"' { + b = append(b, c) + cmd = cmd[1:] + } + inquote = !inquote + } else { + b = append(b, c) + } + nslash = 0 + continue + case '\\': + nslash++ + continue + } + b = appendBSBytes(b, nslash) + nslash = 0 + b = append(b, c) + } + return appendBSBytes(b, nslash), "" +} + +// commandLineToArgv splits a command line into individual argument +// strings, following the Windows conventions documented +// at http://daviddeley.com/autohotkey/parameters/parameters.htm#WINARGV +func commandLineToArgv(cmd string) []string { + var args []string + for len(cmd) > 0 { + if cmd[0] == ' ' || cmd[0] == '\t' { + cmd = cmd[1:] + continue + } + var arg []byte + arg, cmd = readNextArg(cmd) + args = append(args, string(arg)) + } + return args +} + +func ftToDuration(ft *syscall.Filetime) time.Duration { + n := int64(ft.HighDateTime)<<32 + int64(ft.LowDateTime) // in 100-nanosecond intervals + return time.Duration(n*100) * time.Nanosecond +} + +func (p *ProcessState) userTime() time.Duration { + return ftToDuration(&p.rusage.UserTime) +} + +func (p *ProcessState) systemTime() time.Duration { + return ftToDuration(&p.rusage.KernelTime) +} diff --git a/internal/stdlib/os/str.go b/internal/stdlib/os/str.go new file mode 100644 index 0000000000..35643e0d2f --- /dev/null +++ b/internal/stdlib/os/str.go @@ -0,0 +1,39 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Simple conversions to avoid depending on strconv. + +package os + +// itox converts val (an int) to a hexdecimal string. +func itox(val int) string { + if val < 0 { + return "-" + uitox(uint(-val)) + } + return uitox(uint(val)) +} + +const hex = "0123456789abcdef" + +// uitox converts val (a uint) to a hexdecimal string. +func uitox(val uint) string { + if val == 0 { // avoid string allocation + return "0x0" + } + var buf [20]byte // big enough for 64bit value base 16 + 0x + i := len(buf) - 1 + for val >= 16 { + q := val / 16 + buf[i] = hex[val%16] + i-- + val = q + } + // val < 16 + buf[i] = hex[val%16] + i-- + buf[i] = 'x' + i-- + buf[i] = '0' + return string(buf[i:]) +} diff --git a/internal/stdlib/syscall/exec_windows.go b/internal/stdlib/syscall/exec_windows.go new file mode 100644 index 0000000000..18d15028c3 --- /dev/null +++ b/internal/stdlib/syscall/exec_windows.go @@ -0,0 +1,420 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Fork, exec, wait, etc. + +package syscall + +import ( + "runtime" + "sync" + "unicode/utf16" + "unsafe" +) + +var ForkLock sync.RWMutex + +// EscapeArg rewrites command line argument s as prescribed +// in https://msdn.microsoft.com/en-us/library/ms880421. +// This function returns "" (2 double quotes) if s is empty. +// Alternatively, these transformations are done: +// - every back slash (\) is doubled, but only if immediately +// followed by double quote ("); +// - every double quote (") is escaped by back slash (\); +// - finally, s is wrapped with double quotes (arg -> "arg"), +// but only if there is space or tab inside s. +func EscapeArg(s string) string { + if len(s) == 0 { + return `""` + } + for i := 0; i < len(s); i++ { + switch s[i] { + case '"', '\\', ' ', '\t': + // Some escaping required. + b := make([]byte, 0, len(s)+2) + b = appendEscapeArg(b, s) + return string(b) + } + } + return s +} + +// appendEscapeArg escapes the string s, as per escapeArg, +// appends the result to b, and returns the updated slice. +func appendEscapeArg(b []byte, s string) []byte { + if len(s) == 0 { + return append(b, `""`...) + } + + needsBackslash := false + hasSpace := false + for i := 0; i < len(s); i++ { + switch s[i] { + case '"', '\\': + needsBackslash = true + case ' ', '\t': + hasSpace = true + } + } + + if !needsBackslash && !hasSpace { + // No special handling required; normal case. + return append(b, s...) + } + if !needsBackslash { + // hasSpace is true, so we need to quote the string. + b = append(b, '"') + b = append(b, s...) + return append(b, '"') + } + + if hasSpace { + b = append(b, '"') + } + slashes := 0 + for i := 0; i < len(s); i++ { + c := s[i] + switch c { + default: + slashes = 0 + case '\\': + slashes++ + case '"': + for ; slashes > 0; slashes-- { + b = append(b, '\\') + } + b = append(b, '\\') + } + b = append(b, c) + } + if hasSpace { + for ; slashes > 0; slashes-- { + b = append(b, '\\') + } + b = append(b, '"') + } + + return b +} + +// makeCmdLine builds a command line out of args by escaping "special" +// characters and joining the arguments with spaces. +func makeCmdLine(args []string) string { + var b []byte + for _, v := range args { + if len(b) > 0 { + b = append(b, ' ') + } + b = appendEscapeArg(b, v) + } + return string(b) +} + +// createEnvBlock converts an array of environment strings into +// the representation required by CreateProcess: a sequence of NUL +// terminated strings followed by a nil. +// Last bytes are two UCS-2 NULs, or four NUL bytes. +func createEnvBlock(envv []string) *uint16 { + if len(envv) == 0 { + return &utf16.Encode([]rune("\x00\x00"))[0] + } + length := 0 + for _, s := range envv { + length += len(s) + 1 + } + length += 1 + + b := make([]byte, length) + i := 0 + for _, s := range envv { + l := len(s) + copy(b[i:i+l], []byte(s)) + copy(b[i+l:i+l+1], []byte{0}) + i = i + l + 1 + } + copy(b[i:i+1], []byte{0}) + + return &utf16.Encode([]rune(string(b)))[0] +} + +func CloseOnExec(fd Handle) { + SetHandleInformation(Handle(fd), HANDLE_FLAG_INHERIT, 0) +} + +func SetNonblock(fd Handle, nonblocking bool) (err error) { + return nil +} + +// FullPath retrieves the full path of the specified file. +func FullPath(name string) (path string, err error) { + p, err := UTF16PtrFromString(name) + if err != nil { + return "", err + } + n := uint32(100) + for { + buf := make([]uint16, n) + n, err = GetFullPathName(p, uint32(len(buf)), &buf[0], nil) + if err != nil { + return "", err + } + if n <= uint32(len(buf)) { + return UTF16ToString(buf[:n]), nil + } + } +} + +func isSlash(c uint8) bool { + return c == '\\' || c == '/' +} + +func normalizeDir(dir string) (name string, err error) { + ndir, err := FullPath(dir) + if err != nil { + return "", err + } + if len(ndir) > 2 && isSlash(ndir[0]) && isSlash(ndir[1]) { + // dir cannot have \\server\share\path form + return "", EINVAL + } + return ndir, nil +} + +func volToUpper(ch int) int { + if 'a' <= ch && ch <= 'z' { + ch += 'A' - 'a' + } + return ch +} + +func joinExeDirAndFName(dir, p string) (name string, err error) { + if len(p) == 0 { + return "", EINVAL + } + if len(p) > 2 && isSlash(p[0]) && isSlash(p[1]) { + // \\server\share\path form + return p, nil + } + if len(p) > 1 && p[1] == ':' { + // has drive letter + if len(p) == 2 { + return "", EINVAL + } + if isSlash(p[2]) { + return p, nil + } else { + d, err := normalizeDir(dir) + if err != nil { + return "", err + } + if volToUpper(int(p[0])) == volToUpper(int(d[0])) { + return FullPath(d + "\\" + p[2:]) + } else { + return FullPath(p) + } + } + } else { + // no drive letter + d, err := normalizeDir(dir) + if err != nil { + return "", err + } + if isSlash(p[0]) { + return FullPath(d[:2] + p) + } else { + return FullPath(d + "\\" + p) + } + } +} + +type ProcAttr struct { + Dir string + Env []string + Files []uintptr + Sys *SysProcAttr +} + +type SysProcAttr struct { + HideWindow bool + CmdLine string // used if non-empty, else the windows command line is built by escaping the arguments passed to StartProcess + CreationFlags uint32 + Token Token // if set, runs new process in the security context represented by the token + ProcessAttributes *SecurityAttributes // if set, applies these security attributes as the descriptor for the new process + ThreadAttributes *SecurityAttributes // if set, applies these security attributes as the descriptor for the main thread of the new process + NoInheritHandles bool // if set, each inheritable handle in the calling process is not inherited by the new process + AdditionalInheritedHandles []Handle // a list of additional handles, already marked as inheritable, that will be inherited by the new process + ParentProcess Handle // if non-zero, the new process regards the process given by this handle as its parent process, and AdditionalInheritedHandles, if set, should exist in this parent process +} + +var zeroProcAttr ProcAttr +var zeroSysProcAttr SysProcAttr + +func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle uintptr, err error) { + if len(argv0) == 0 { + return 0, 0, EWINDOWS + } + if attr == nil { + attr = &zeroProcAttr + } + sys := attr.Sys + if sys == nil { + sys = &zeroSysProcAttr + } + + if len(attr.Files) > 3 { + return 0, 0, EWINDOWS + } + if len(attr.Files) < 3 { + return 0, 0, EINVAL + } + + if len(attr.Dir) != 0 { + // StartProcess assumes that argv0 is relative to attr.Dir, + // because it implies Chdir(attr.Dir) before executing argv0. + // Windows CreateProcess assumes the opposite: it looks for + // argv0 relative to the current directory, and, only once the new + // process is started, it does Chdir(attr.Dir). We are adjusting + // for that difference here by making argv0 absolute. + var err error + argv0, err = joinExeDirAndFName(attr.Dir, argv0) + if err != nil { + return 0, 0, err + } + } + argv0p, err := UTF16PtrFromString(argv0) + if err != nil { + return 0, 0, err + } + + var cmdline string + // Windows CreateProcess takes the command line as a single string: + // use attr.CmdLine if set, else build the command line by escaping + // and joining each argument with spaces + if sys.CmdLine != "" { + cmdline = sys.CmdLine + } else { + cmdline = makeCmdLine(argv) + } + + var argvp *uint16 + if len(cmdline) != 0 { + argvp, err = UTF16PtrFromString(cmdline) + if err != nil { + return 0, 0, err + } + } + + var dirp *uint16 + if len(attr.Dir) != 0 { + dirp, err = UTF16PtrFromString(attr.Dir) + if err != nil { + return 0, 0, err + } + } + + var maj, min, build uint32 + rtlGetNtVersionNumbers(&maj, &min, &build) + isWin7 := maj < 6 || (maj == 6 && min <= 1) + // NT kernel handles are divisible by 4, with the bottom 3 bits left as + // a tag. The fully set tag correlates with the types of handles we're + // concerned about here. Except, the kernel will interpret some + // special handle values, like -1, -2, and so forth, so kernelbase.dll + // checks to see that those bottom three bits are checked, but that top + // bit is not checked. + isLegacyWin7ConsoleHandle := func(handle Handle) bool { return isWin7 && handle&0x10000003 == 3 } + + p, _ := GetCurrentProcess() + parentProcess := p + if sys.ParentProcess != 0 { + parentProcess = sys.ParentProcess + } + fd := make([]Handle, len(attr.Files)) + for i := range attr.Files { + if attr.Files[i] > 0 { + destinationProcessHandle := parentProcess + + // On Windows 7, console handles aren't real handles, and can only be duplicated + // into the current process, not a parent one, which amounts to the same thing. + if parentProcess != p && isLegacyWin7ConsoleHandle(Handle(attr.Files[i])) { + destinationProcessHandle = p + } + + err := DuplicateHandle(p, Handle(attr.Files[i]), destinationProcessHandle, &fd[i], 0, true, DUPLICATE_SAME_ACCESS) + if err != nil { + return 0, 0, err + } + defer DuplicateHandle(parentProcess, fd[i], 0, nil, 0, false, DUPLICATE_CLOSE_SOURCE) + } + } + si := new(_STARTUPINFOEXW) + si.ProcThreadAttributeList, err = newProcThreadAttributeList(2) + if err != nil { + return 0, 0, err + } + defer deleteProcThreadAttributeList(si.ProcThreadAttributeList) + si.Cb = uint32(unsafe.Sizeof(*si)) + si.Flags = STARTF_USESTDHANDLES + if sys.HideWindow { + si.Flags |= STARTF_USESHOWWINDOW + si.ShowWindow = SW_HIDE + } + if sys.ParentProcess != 0 { + err = updateProcThreadAttribute(si.ProcThreadAttributeList, 0, _PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, unsafe.Pointer(&sys.ParentProcess), unsafe.Sizeof(sys.ParentProcess), nil, nil) + if err != nil { + return 0, 0, err + } + } + si.StdInput = fd[0] + si.StdOutput = fd[1] + si.StdErr = fd[2] + + fd = append(fd, sys.AdditionalInheritedHandles...) + + // On Windows 7, console handles aren't real handles, so don't pass them + // through to PROC_THREAD_ATTRIBUTE_HANDLE_LIST. + for i := range fd { + if isLegacyWin7ConsoleHandle(fd[i]) { + fd[i] = 0 + } + } + + // The presence of a NULL handle in the list is enough to cause PROC_THREAD_ATTRIBUTE_HANDLE_LIST + // to treat the entire list as empty, so remove NULL handles. + j := 0 + for i := range fd { + if fd[i] != 0 { + fd[j] = fd[i] + j++ + } + } + fd = fd[:j] + + // Do not accidentally inherit more than these handles. + if len(fd) > 0 { + err = updateProcThreadAttribute(si.ProcThreadAttributeList, 0, _PROC_THREAD_ATTRIBUTE_HANDLE_LIST, unsafe.Pointer(&fd[0]), uintptr(len(fd))*unsafe.Sizeof(fd[0]), nil, nil) + if err != nil { + return 0, 0, err + } + } + + pi := new(ProcessInformation) + flags := sys.CreationFlags | CREATE_UNICODE_ENVIRONMENT | _EXTENDED_STARTUPINFO_PRESENT + if sys.Token != 0 { + err = CreateProcessAsUser(sys.Token, argv0p, argvp, sys.ProcessAttributes, sys.ThreadAttributes, len(fd) > 0 && !sys.NoInheritHandles, flags, createEnvBlock(attr.Env), dirp, &si.StartupInfo, pi) + } else { + err = CreateProcess(argv0p, argvp, sys.ProcessAttributes, sys.ThreadAttributes, len(fd) > 0 && !sys.NoInheritHandles, flags, createEnvBlock(attr.Env), dirp, &si.StartupInfo, pi) + } + if err != nil { + return 0, 0, err + } + defer CloseHandle(Handle(pi.Thread)) + runtime.KeepAlive(fd) + runtime.KeepAlive(sys) + + return int(pi.ProcessId), uintptr(pi.Process), nil +} + +func Exec(argv0 string, argv []string, envv []string) (err error) { + return EWINDOWS +} From 6906be1d44d96d714d6419619bc2ca9322b40613 Mon Sep 17 00:00:00 2001 From: Daniel Canter Date: Thu, 26 Aug 2021 05:51:55 -0700 Subject: [PATCH 2/2] Get stdlib fork adjusted and working This change edits a whole bunch of stuff in the stdlib fork to get things actually building. This includes things such as removing the imports to /internal packages in the stdlib (as we can't access them), changing around the fields of some of the calls to point to our forked version instead of the stdlib, and changing a whole slew of syscall definitions to x/sys/windows equivalents. Also satisfying golangci-lint by ignoring some errors and removing dead code that isn't used. Additionally adds a small test to make sure it works. Signed-off-by: Daniel Canter --- internal/stdlib/README.md | 3 +- internal/stdlib/execenv/execenv_windows.go | 10 +-- internal/stdlib/os/exec.go | 19 ++--- internal/stdlib/os/exec/exec.go | 19 +++-- internal/stdlib/os/exec/exec_test.go | 11 +++ internal/stdlib/os/exec_posix.go | 18 ++-- internal/stdlib/os/exec_windows.go | 95 +++------------------- internal/stdlib/os/str.go | 8 -- internal/stdlib/syscall/exec_windows.go | 89 ++++++++++---------- internal/winapi/process.go | 2 + internal/winapi/zsyscall_windows.go | 19 +++++ 11 files changed, 126 insertions(+), 167 deletions(-) create mode 100644 internal/stdlib/os/exec/exec_test.go diff --git a/internal/stdlib/README.md b/internal/stdlib/README.md index bb21944c32..ed87ab4c1d 100644 --- a/internal/stdlib/README.md +++ b/internal/stdlib/README.md @@ -22,7 +22,8 @@ and the syscall package is locked down for the most part. 3. Almost same story as 2, but in this case we'd like to support assigning a [pseudo console](https://docs.microsoft.com/en-us/windows/console/createpseudoconsole) to a process. There's no exposed way to pass in the pseudo console handle and there's no syscall package support for making one in the first place. + The stdlib packages have been modified to use the x/sys/windows package where needed, removed some unneccesary functionality that we don't need, as well as added the additions described above. -The fork is based off of go 1.17 with HEAD at a6ff433d6a927e8ad8eaa6828127233296d12ce5. \ No newline at end of file +The fork is from Go 1.17 with HEAD at a6ff433d6a927e8ad8eaa6828127233296d12ce5. \ No newline at end of file diff --git a/internal/stdlib/execenv/execenv_windows.go b/internal/stdlib/execenv/execenv_windows.go index 3b79a91104..993076185f 100644 --- a/internal/stdlib/execenv/execenv_windows.go +++ b/internal/stdlib/execenv/execenv_windows.go @@ -8,11 +8,11 @@ package execenv import ( - "syscall" "unicode/utf16" "unsafe" - "internal/syscall/windows" + "github.com/Microsoft/hcsshim/internal/stdlib/syscall" + "golang.org/x/sys/windows" ) // Default will return the default environment @@ -25,14 +25,14 @@ import ( // will be sourced from syscall.Environ(). func Default(sys *syscall.SysProcAttr) (env []string, err error) { if sys == nil || sys.Token == 0 { - return syscall.Environ(), nil + return windows.Environ(), nil } var block *uint16 - err = windows.CreateEnvironmentBlock(&block, windows.Token(sys.Token), false) + err = windows.CreateEnvironmentBlock(&block, sys.Token, false) if err != nil { return nil, err } - defer windows.DestroyEnvironmentBlock(block) + defer windows.DestroyEnvironmentBlock(block) // nolint: errcheck blockp := uintptr(unsafe.Pointer(block)) for { diff --git a/internal/stdlib/os/exec.go b/internal/stdlib/os/exec.go index bc75d4dd66..754347eb5e 100644 --- a/internal/stdlib/os/exec.go +++ b/internal/stdlib/os/exec.go @@ -6,12 +6,13 @@ package os import ( "errors" - "internal/testlog" + "os" "runtime" - "sync" "sync/atomic" - "syscall" "time" + + "github.com/Microsoft/hcsshim/internal/stdlib/syscall" + "golang.org/x/sys/windows" ) // ErrProcessDone indicates a Process has finished. @@ -20,9 +21,8 @@ var ErrProcessDone = errors.New("os: process already finished") // Process stores the information about a process created by StartProcess. type Process struct { Pid int - handle uintptr // handle is accessed atomically on Windows - isdone uint32 // process has been successfully waited on, non zero if true - sigMu sync.RWMutex // avoid race between wait and signal + handle uintptr // handle is accessed atomically on Windows + isdone uint32 // process has been successfully waited on, non zero if true } func newProcess(pid int, handle uintptr) *Process { @@ -57,7 +57,7 @@ type ProcAttr struct { // On Unix systems, StartProcess will change these File values // to blocking mode, which means that SetDeadline will stop working // and calling Close will not interrupt a Read or Write. - Files []*File + Files []*os.File // Operating system-specific process creation attributes. // Note that setting this field means that your program @@ -75,10 +75,10 @@ type Signal interface { } // Getpid returns the process id of the caller. -func Getpid() int { return syscall.Getpid() } +func Getpid() int { return windows.Getpid() } // Getppid returns the process id of the caller's parent. -func Getppid() int { return syscall.Getppid() } +func Getppid() int { return windows.Getppid() } // FindProcess looks for a running process by its pid. // @@ -105,7 +105,6 @@ func FindProcess(pid int) (*Process, error) { // // If there is an error, it will be of type *PathError. func StartProcess(name string, argv []string, attr *ProcAttr) (*Process, error) { - testlog.Open(name) return startProcess(name, argv, attr) } diff --git a/internal/stdlib/os/exec/exec.go b/internal/stdlib/os/exec/exec.go index bc9fd5aa96..35f669b010 100644 --- a/internal/stdlib/os/exec/exec.go +++ b/internal/stdlib/os/exec/exec.go @@ -24,7 +24,6 @@ import ( "bytes" "context" "errors" - "internal/syscall/execenv "io" "os" "path/filepath" @@ -32,7 +31,11 @@ import ( "strconv" "strings" "sync" - "syscall" + + "github.com/Microsoft/hcsshim/internal/stdlib/execenv" + "github.com/Microsoft/hcsshim/internal/stdlib/syscall" + + osfork "github.com/Microsoft/hcsshim/internal/stdlib/os" ) // Error is returned by LookPath when it fails to classify a file as an @@ -127,11 +130,11 @@ type Cmd struct { SysProcAttr *syscall.SysProcAttr // Process is the underlying process, once started. - Process *os.Process + Process *osfork.Process // ProcessState contains information about an exited process, // available after a call to Wait or Run. - ProcessState *os.ProcessState + ProcessState *osfork.ProcessState ctx context.Context // nil means none lookPathErr error // LookPath error, if any. @@ -218,7 +221,7 @@ func (c *Cmd) String() string { // two interfaces with non-comparable underlying types. func interfaceEqual(a, b interface{}) bool { defer func() { - recover() + recover() // nolint: errcheck }() return a == b } @@ -419,7 +422,7 @@ func (c *Cmd) Start() error { return err } - c.Process, err = os.StartProcess(c.Path, c.argv(), &os.ProcAttr{ + c.Process, err = osfork.StartProcess(c.Path, c.argv(), &osfork.ProcAttr{ Dir: c.Dir, Files: c.childFiles, Env: addCriticalEnv(dedupEnv(envv)), @@ -448,7 +451,7 @@ func (c *Cmd) Start() error { go func() { select { case <-c.ctx.Done(): - c.Process.Kill() + c.Process.Kill() // nolint: errcheck case <-c.waitDone: } }() @@ -459,7 +462,7 @@ func (c *Cmd) Start() error { // An ExitError reports an unsuccessful exit by a command. type ExitError struct { - *os.ProcessState + *osfork.ProcessState // Stderr holds a subset of the standard error output from the // Cmd.Output method if standard error was not otherwise being diff --git a/internal/stdlib/os/exec/exec_test.go b/internal/stdlib/os/exec/exec_test.go new file mode 100644 index 0000000000..0eeee0a41d --- /dev/null +++ b/internal/stdlib/os/exec/exec_test.go @@ -0,0 +1,11 @@ +package exec + +import "testing" + +// Very rudimentary test that the os/exec fork works. +func TestExec(t *testing.T) { + cmd := Command("ping", "127.0.0.1") + if err := cmd.Run(); err != nil { + t.Fatal(err) + } +} diff --git a/internal/stdlib/os/exec_posix.go b/internal/stdlib/os/exec_posix.go index e8736f7c54..110919585c 100644 --- a/internal/stdlib/os/exec_posix.go +++ b/internal/stdlib/os/exec_posix.go @@ -8,10 +8,14 @@ package os import ( - "internal/itoa" - "internal/syscall/execenv" + "os" "runtime" "syscall" + + "github.com/Microsoft/hcsshim/internal/stdlib/execenv" + syscallfork "github.com/Microsoft/hcsshim/internal/stdlib/syscall" + + "github.com/Microsoft/hcsshim/internal/stdlib/itoa" ) // The only signal values guaranteed to be present in the os package on all @@ -29,14 +33,14 @@ func startProcess(name string, argv []string, attr *ProcAttr) (p *Process, err e // UID/GID), double-check existence of the directory we want // to chdir into. We can make the error clearer this way. if attr != nil && attr.Sys == nil && attr.Dir != "" { - if _, err := Stat(attr.Dir); err != nil { - pe := err.(*PathError) + if _, err := os.Stat(attr.Dir); err != nil { + pe := err.(*os.PathError) pe.Op = "chdir" return nil, pe } } - sysattr := &syscall.ProcAttr{ + sysattr := &syscallfork.ProcAttr{ Dir: attr.Dir, Env: attr.Env, Sys: attr.Sys, @@ -52,13 +56,13 @@ func startProcess(name string, argv []string, attr *ProcAttr) (p *Process, err e sysattr.Files = append(sysattr.Files, f.Fd()) } - pid, h, e := syscall.StartProcess(name, argv, sysattr) + pid, h, e := syscallfork.StartProcess(name, argv, sysattr) // Make sure we don't run the finalizers of attr.Files. runtime.KeepAlive(attr) if e != nil { - return nil, &PathError{Op: "fork/exec", Path: name, Err: e} + return nil, &os.PathError{Op: "fork/exec", Path: name, Err: e} } return newProcess(pid, h), nil diff --git a/internal/stdlib/os/exec_windows.go b/internal/stdlib/os/exec_windows.go index 239bed198f..89a496c4dd 100644 --- a/internal/stdlib/os/exec_windows.go +++ b/internal/stdlib/os/exec_windows.go @@ -6,7 +6,7 @@ package os import ( "errors" - "internal/syscall/windows" + "os" "runtime" "sync/atomic" "syscall" @@ -20,19 +20,19 @@ func (p *Process) wait() (ps *ProcessState, err error) { case syscall.WAIT_OBJECT_0: break case syscall.WAIT_FAILED: - return nil, NewSyscallError("WaitForSingleObject", e) + return nil, os.NewSyscallError("WaitForSingleObject", e) default: return nil, errors.New("os: unexpected result from WaitForSingleObject") } var ec uint32 e = syscall.GetExitCodeProcess(syscall.Handle(handle), &ec) if e != nil { - return nil, NewSyscallError("GetExitCodeProcess", e) + return nil, os.NewSyscallError("GetExitCodeProcess", e) } var u syscall.Rusage e = syscall.GetProcessTimes(syscall.Handle(handle), &u.CreationTime, &u.ExitTime, &u.KernelTime, &u.UserTime) if e != nil { - return nil, NewSyscallError("GetProcessTimes", e) + return nil, os.NewSyscallError("GetProcessTimes", e) } p.setDone() // NOTE(brainman): It seems that sometimes process is not dead @@ -41,7 +41,7 @@ func (p *Process) wait() (ps *ProcessState, err error) { // the trick sometimes. // See https://golang.org/issue/25965 for details. defer time.Sleep(5 * time.Millisecond) - defer p.Release() + defer p.Release() // nolint: errcheck return &ProcessState{p.Pid, syscall.WaitStatus{ExitCode: ec}, &u}, nil } @@ -57,12 +57,12 @@ func (p *Process) signal(sig Signal) error { var terminationHandle syscall.Handle e := syscall.DuplicateHandle(^syscall.Handle(0), syscall.Handle(handle), ^syscall.Handle(0), &terminationHandle, syscall.PROCESS_TERMINATE, false, 0) if e != nil { - return NewSyscallError("DuplicateHandle", e) + return os.NewSyscallError("DuplicateHandle", e) } runtime.KeepAlive(p) - defer syscall.CloseHandle(terminationHandle) + defer syscall.CloseHandle(terminationHandle) // nolint: errcheck e = syscall.TerminateProcess(syscall.Handle(terminationHandle), 1) - return NewSyscallError("TerminateProcess", e) + return os.NewSyscallError("TerminateProcess", e) } // TODO(rsc): Handle Interrupt too? return syscall.Errno(syscall.EWINDOWS) @@ -75,7 +75,7 @@ func (p *Process) release() error { } e := syscall.CloseHandle(syscall.Handle(handle)) if e != nil { - return NewSyscallError("CloseHandle", e) + return os.NewSyscallError("CloseHandle", e) } // no need for a finalizer anymore runtime.SetFinalizer(p, nil) @@ -87,86 +87,11 @@ func findProcess(pid int) (p *Process, err error) { syscall.PROCESS_QUERY_INFORMATION | syscall.SYNCHRONIZE h, e := syscall.OpenProcess(da, false, uint32(pid)) if e != nil { - return nil, NewSyscallError("OpenProcess", e) + return nil, os.NewSyscallError("OpenProcess", e) } return newProcess(pid, uintptr(h)), nil } -func init() { - cmd := windows.UTF16PtrToString(syscall.GetCommandLine()) - if len(cmd) == 0 { - arg0, _ := Executable() - Args = []string{arg0} - } else { - Args = commandLineToArgv(cmd) - } -} - -// appendBSBytes appends n '\\' bytes to b and returns the resulting slice. -func appendBSBytes(b []byte, n int) []byte { - for ; n > 0; n-- { - b = append(b, '\\') - } - return b -} - -// readNextArg splits command line string cmd into next -// argument and command line remainder. -func readNextArg(cmd string) (arg []byte, rest string) { - var b []byte - var inquote bool - var nslash int - for ; len(cmd) > 0; cmd = cmd[1:] { - c := cmd[0] - switch c { - case ' ', '\t': - if !inquote { - return appendBSBytes(b, nslash), cmd[1:] - } - case '"': - b = appendBSBytes(b, nslash/2) - if nslash%2 == 0 { - // use "Prior to 2008" rule from - // http://daviddeley.com/autohotkey/parameters/parameters.htm - // section 5.2 to deal with double double quotes - if inquote && len(cmd) > 1 && cmd[1] == '"' { - b = append(b, c) - cmd = cmd[1:] - } - inquote = !inquote - } else { - b = append(b, c) - } - nslash = 0 - continue - case '\\': - nslash++ - continue - } - b = appendBSBytes(b, nslash) - nslash = 0 - b = append(b, c) - } - return appendBSBytes(b, nslash), "" -} - -// commandLineToArgv splits a command line into individual argument -// strings, following the Windows conventions documented -// at http://daviddeley.com/autohotkey/parameters/parameters.htm#WINARGV -func commandLineToArgv(cmd string) []string { - var args []string - for len(cmd) > 0 { - if cmd[0] == ' ' || cmd[0] == '\t' { - cmd = cmd[1:] - continue - } - var arg []byte - arg, cmd = readNextArg(cmd) - args = append(args, string(arg)) - } - return args -} - func ftToDuration(ft *syscall.Filetime) time.Duration { n := int64(ft.HighDateTime)<<32 + int64(ft.LowDateTime) // in 100-nanosecond intervals return time.Duration(n*100) * time.Nanosecond diff --git a/internal/stdlib/os/str.go b/internal/stdlib/os/str.go index 35643e0d2f..840ac1ef74 100644 --- a/internal/stdlib/os/str.go +++ b/internal/stdlib/os/str.go @@ -6,14 +6,6 @@ package os -// itox converts val (an int) to a hexdecimal string. -func itox(val int) string { - if val < 0 { - return "-" + uitox(uint(-val)) - } - return uitox(uint(val)) -} - const hex = "0123456789abcdef" // uitox converts val (a uint) to a hexdecimal string. diff --git a/internal/stdlib/syscall/exec_windows.go b/internal/stdlib/syscall/exec_windows.go index 18d15028c3..c8fcea43d6 100644 --- a/internal/stdlib/syscall/exec_windows.go +++ b/internal/stdlib/syscall/exec_windows.go @@ -9,8 +9,12 @@ package syscall import ( "runtime" "sync" + "syscall" "unicode/utf16" "unsafe" + + "github.com/Microsoft/hcsshim/internal/winapi" + "golang.org/x/sys/windows" ) var ForkLock sync.RWMutex @@ -138,29 +142,29 @@ func createEnvBlock(envv []string) *uint16 { return &utf16.Encode([]rune(string(b)))[0] } -func CloseOnExec(fd Handle) { - SetHandleInformation(Handle(fd), HANDLE_FLAG_INHERIT, 0) +func CloseOnExec(fd windows.Handle) { + windows.SetHandleInformation(windows.Handle(fd), windows.HANDLE_FLAG_INHERIT, 0) // nolint: errcheck } -func SetNonblock(fd Handle, nonblocking bool) (err error) { +func SetNonblock(fd windows.Handle, nonblocking bool) (err error) { return nil } // FullPath retrieves the full path of the specified file. func FullPath(name string) (path string, err error) { - p, err := UTF16PtrFromString(name) + p, err := windows.UTF16PtrFromString(name) if err != nil { return "", err } n := uint32(100) for { buf := make([]uint16, n) - n, err = GetFullPathName(p, uint32(len(buf)), &buf[0], nil) + n, err = windows.GetFullPathName(p, uint32(len(buf)), &buf[0], nil) if err != nil { return "", err } if n <= uint32(len(buf)) { - return UTF16ToString(buf[:n]), nil + return windows.UTF16ToString(buf[:n]), nil } } } @@ -176,7 +180,7 @@ func normalizeDir(dir string) (name string, err error) { } if len(ndir) > 2 && isSlash(ndir[0]) && isSlash(ndir[1]) { // dir cannot have \\server\share\path form - return "", EINVAL + return "", syscall.EINVAL } return ndir, nil } @@ -190,7 +194,7 @@ func volToUpper(ch int) int { func joinExeDirAndFName(dir, p string) (name string, err error) { if len(p) == 0 { - return "", EINVAL + return "", syscall.EINVAL } if len(p) > 2 && isSlash(p[0]) && isSlash(p[1]) { // \\server\share\path form @@ -199,7 +203,7 @@ func joinExeDirAndFName(dir, p string) (name string, err error) { if len(p) > 1 && p[1] == ':' { // has drive letter if len(p) == 2 { - return "", EINVAL + return "", syscall.EINVAL } if isSlash(p[2]) { return p, nil @@ -239,12 +243,12 @@ type SysProcAttr struct { HideWindow bool CmdLine string // used if non-empty, else the windows command line is built by escaping the arguments passed to StartProcess CreationFlags uint32 - Token Token // if set, runs new process in the security context represented by the token - ProcessAttributes *SecurityAttributes // if set, applies these security attributes as the descriptor for the new process - ThreadAttributes *SecurityAttributes // if set, applies these security attributes as the descriptor for the main thread of the new process - NoInheritHandles bool // if set, each inheritable handle in the calling process is not inherited by the new process - AdditionalInheritedHandles []Handle // a list of additional handles, already marked as inheritable, that will be inherited by the new process - ParentProcess Handle // if non-zero, the new process regards the process given by this handle as its parent process, and AdditionalInheritedHandles, if set, should exist in this parent process + Token windows.Token // if set, runs new process in the security context represented by the token + ProcessAttributes *windows.SecurityAttributes // if set, applies these security attributes as the descriptor for the new process + ThreadAttributes *windows.SecurityAttributes // if set, applies these security attributes as the descriptor for the main thread of the new process + NoInheritHandles bool // if set, each inheritable handle in the calling process is not inherited by the new process + AdditionalInheritedHandles []windows.Handle // a list of additional handles, already marked as inheritable, that will be inherited by the new process + ParentProcess windows.Handle // if non-zero, the new process regards the process given by this handle as its parent process, and AdditionalInheritedHandles, if set, should exist in this parent process } var zeroProcAttr ProcAttr @@ -252,7 +256,7 @@ var zeroSysProcAttr SysProcAttr func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle uintptr, err error) { if len(argv0) == 0 { - return 0, 0, EWINDOWS + return 0, 0, syscall.EINVAL } if attr == nil { attr = &zeroProcAttr @@ -263,10 +267,10 @@ func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle } if len(attr.Files) > 3 { - return 0, 0, EWINDOWS + return 0, 0, syscall.EINVAL } if len(attr.Files) < 3 { - return 0, 0, EINVAL + return 0, 0, syscall.EINVAL } if len(attr.Dir) != 0 { @@ -282,7 +286,7 @@ func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle return 0, 0, err } } - argv0p, err := UTF16PtrFromString(argv0) + argv0p, err := windows.UTF16PtrFromString(argv0) if err != nil { return 0, 0, err } @@ -299,7 +303,7 @@ func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle var argvp *uint16 if len(cmdline) != 0 { - argvp, err = UTF16PtrFromString(cmdline) + argvp, err = windows.UTF16PtrFromString(cmdline) if err != nil { return 0, 0, err } @@ -307,14 +311,13 @@ func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle var dirp *uint16 if len(attr.Dir) != 0 { - dirp, err = UTF16PtrFromString(attr.Dir) + dirp, err = windows.UTF16PtrFromString(attr.Dir) if err != nil { return 0, 0, err } } - var maj, min, build uint32 - rtlGetNtVersionNumbers(&maj, &min, &build) + maj, min, _ := windows.RtlGetNtVersionNumbers() isWin7 := maj < 6 || (maj == 6 && min <= 1) // NT kernel handles are divisible by 4, with the bottom 3 bits left as // a tag. The fully set tag correlates with the types of handles we're @@ -322,45 +325,45 @@ func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle // special handle values, like -1, -2, and so forth, so kernelbase.dll // checks to see that those bottom three bits are checked, but that top // bit is not checked. - isLegacyWin7ConsoleHandle := func(handle Handle) bool { return isWin7 && handle&0x10000003 == 3 } + isLegacyWin7ConsoleHandle := func(handle windows.Handle) bool { return isWin7 && handle&0x10000003 == 3 } - p, _ := GetCurrentProcess() + p := windows.CurrentProcess() parentProcess := p if sys.ParentProcess != 0 { parentProcess = sys.ParentProcess } - fd := make([]Handle, len(attr.Files)) + fd := make([]windows.Handle, len(attr.Files)) for i := range attr.Files { if attr.Files[i] > 0 { destinationProcessHandle := parentProcess // On Windows 7, console handles aren't real handles, and can only be duplicated // into the current process, not a parent one, which amounts to the same thing. - if parentProcess != p && isLegacyWin7ConsoleHandle(Handle(attr.Files[i])) { + if parentProcess != p && isLegacyWin7ConsoleHandle(windows.Handle(attr.Files[i])) { destinationProcessHandle = p } - err := DuplicateHandle(p, Handle(attr.Files[i]), destinationProcessHandle, &fd[i], 0, true, DUPLICATE_SAME_ACCESS) + err := windows.DuplicateHandle(p, windows.Handle(attr.Files[i]), destinationProcessHandle, &fd[i], 0, true, windows.DUPLICATE_SAME_ACCESS) if err != nil { return 0, 0, err } - defer DuplicateHandle(parentProcess, fd[i], 0, nil, 0, false, DUPLICATE_CLOSE_SOURCE) + defer windows.DuplicateHandle(parentProcess, fd[i], 0, nil, 0, false, windows.DUPLICATE_CLOSE_SOURCE) // nolint: errcheck } } - si := new(_STARTUPINFOEXW) - si.ProcThreadAttributeList, err = newProcThreadAttributeList(2) + si := new(windows.StartupInfoEx) + si.ProcThreadAttributeList, err = windows.NewProcThreadAttributeList(2) if err != nil { return 0, 0, err } - defer deleteProcThreadAttributeList(si.ProcThreadAttributeList) + defer si.ProcThreadAttributeList.Delete() si.Cb = uint32(unsafe.Sizeof(*si)) - si.Flags = STARTF_USESTDHANDLES + si.Flags = windows.STARTF_USESTDHANDLES if sys.HideWindow { - si.Flags |= STARTF_USESHOWWINDOW - si.ShowWindow = SW_HIDE + si.Flags |= windows.STARTF_USESHOWWINDOW + si.ShowWindow = windows.SW_HIDE } if sys.ParentProcess != 0 { - err = updateProcThreadAttribute(si.ProcThreadAttributeList, 0, _PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, unsafe.Pointer(&sys.ParentProcess), unsafe.Sizeof(sys.ParentProcess), nil, nil) + err = si.ProcThreadAttributeList.Update(windows.PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, 0, unsafe.Pointer(&sys.ParentProcess), unsafe.Sizeof(sys.ParentProcess), nil, nil) if err != nil { return 0, 0, err } @@ -392,23 +395,23 @@ func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle // Do not accidentally inherit more than these handles. if len(fd) > 0 { - err = updateProcThreadAttribute(si.ProcThreadAttributeList, 0, _PROC_THREAD_ATTRIBUTE_HANDLE_LIST, unsafe.Pointer(&fd[0]), uintptr(len(fd))*unsafe.Sizeof(fd[0]), nil, nil) + err = si.ProcThreadAttributeList.Update(windows.PROC_THREAD_ATTRIBUTE_HANDLE_LIST, 0, unsafe.Pointer(&fd[0]), uintptr(len(fd))*unsafe.Sizeof(fd[0]), nil, nil) if err != nil { return 0, 0, err } } - pi := new(ProcessInformation) - flags := sys.CreationFlags | CREATE_UNICODE_ENVIRONMENT | _EXTENDED_STARTUPINFO_PRESENT + pi := new(windows.ProcessInformation) + flags := sys.CreationFlags | windows.CREATE_UNICODE_ENVIRONMENT | windows.EXTENDED_STARTUPINFO_PRESENT if sys.Token != 0 { - err = CreateProcessAsUser(sys.Token, argv0p, argvp, sys.ProcessAttributes, sys.ThreadAttributes, len(fd) > 0 && !sys.NoInheritHandles, flags, createEnvBlock(attr.Env), dirp, &si.StartupInfo, pi) + err = winapi.CreateProcessAsUser(sys.Token, argv0p, argvp, sys.ProcessAttributes, sys.ThreadAttributes, len(fd) > 0 && !sys.NoInheritHandles, flags, createEnvBlock(attr.Env), dirp, &si.StartupInfo, pi) } else { - err = CreateProcess(argv0p, argvp, sys.ProcessAttributes, sys.ThreadAttributes, len(fd) > 0 && !sys.NoInheritHandles, flags, createEnvBlock(attr.Env), dirp, &si.StartupInfo, pi) + err = windows.CreateProcess(argv0p, argvp, sys.ProcessAttributes, sys.ThreadAttributes, len(fd) > 0 && !sys.NoInheritHandles, flags, createEnvBlock(attr.Env), dirp, &si.StartupInfo, pi) } if err != nil { return 0, 0, err } - defer CloseHandle(Handle(pi.Thread)) + defer windows.CloseHandle(windows.Handle(pi.Thread)) // nolint: errcheck runtime.KeepAlive(fd) runtime.KeepAlive(sys) @@ -416,5 +419,5 @@ func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle } func Exec(argv0 string, argv []string, envv []string) (err error) { - return EWINDOWS + return syscall.EWINDOWS } diff --git a/internal/winapi/process.go b/internal/winapi/process.go index b87068327c..91277a0349 100644 --- a/internal/winapi/process.go +++ b/internal/winapi/process.go @@ -8,3 +8,5 @@ const PROCESS_ALL_ACCESS uint32 = 2097151 // DWORD nSize // ); //sys GetProcessImageFileName(hProcess windows.Handle, imageFileName *uint16, nSize uint32) (size uint32, err error) = kernel32.GetProcessImageFileNameW + +//sys CreateProcessAsUser(token windows.Token, appName *uint16, commandLine *uint16, procSecurity *windows.SecurityAttributes, threadSecurity *windows.SecurityAttributes, inheritHandles bool, creationFlags uint32, env *uint16, currentDir *uint16, startupInfo *windows.StartupInfo, outProcInfo *windows.ProcessInformation) (err error) = advapi32.CreateProcessAsUserW diff --git a/internal/winapi/zsyscall_windows.go b/internal/winapi/zsyscall_windows.go index 2941b0f980..160dae45ce 100644 --- a/internal/winapi/zsyscall_windows.go +++ b/internal/winapi/zsyscall_windows.go @@ -62,6 +62,7 @@ var ( procLocalFree = modkernel32.NewProc("LocalFree") procQueryWorkingSet = modpsapi.NewProc("QueryWorkingSet") procGetProcessImageFileNameW = modkernel32.NewProc("GetProcessImageFileNameW") + procCreateProcessAsUserW = modadvapi32.NewProc("CreateProcessAsUserW") procGetActiveProcessorCount = modkernel32.NewProc("GetActiveProcessorCount") procCM_Get_Device_ID_List_SizeA = modcfgmgr32.NewProc("CM_Get_Device_ID_List_SizeA") procCM_Get_Device_ID_ListA = modcfgmgr32.NewProc("CM_Get_Device_ID_ListA") @@ -267,6 +268,24 @@ func GetProcessImageFileName(hProcess windows.Handle, imageFileName *uint16, nSi return } +func CreateProcessAsUser(token windows.Token, appName *uint16, commandLine *uint16, procSecurity *windows.SecurityAttributes, threadSecurity *windows.SecurityAttributes, inheritHandles bool, creationFlags uint32, env *uint16, currentDir *uint16, startupInfo *windows.StartupInfo, outProcInfo *windows.ProcessInformation) (err error) { + var _p0 uint32 + if inheritHandles { + _p0 = 1 + } else { + _p0 = 0 + } + r1, _, e1 := syscall.Syscall12(procCreateProcessAsUserW.Addr(), 11, uintptr(token), uintptr(unsafe.Pointer(appName)), uintptr(unsafe.Pointer(commandLine)), uintptr(unsafe.Pointer(procSecurity)), uintptr(unsafe.Pointer(threadSecurity)), uintptr(_p0), uintptr(creationFlags), uintptr(unsafe.Pointer(env)), uintptr(unsafe.Pointer(currentDir)), uintptr(unsafe.Pointer(startupInfo)), uintptr(unsafe.Pointer(outProcInfo)), 0) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + func GetActiveProcessorCount(groupNumber uint16) (amount uint32) { r0, _, _ := syscall.Syscall(procGetActiveProcessorCount.Addr(), 1, uintptr(groupNumber), 0, 0) amount = uint32(r0)