package rpcplugin import ( "context" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" "syscall" "unicode/utf16" "unsafe" pkgerrors "github.com/pkg/errors" ) type process struct { command *cmd } func newProcess(ctx context.Context, path string) (Process, io.ReadWriteCloser, error) { ipc, childFiles, err := NewIPC() if err != nil { return nil, nil, err } defer childFiles[0].Close() defer childFiles[1].Close() cmd := commandContext(ctx, path) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.ExtraFiles = childFiles cmd.Env = append(os.Environ(), fmt.Sprintf("MM_IPC_FD0=%v", childFiles[0].Fd()), fmt.Sprintf("MM_IPC_FD1=%v", childFiles[1].Fd()), ) err = cmd.Start() if err != nil { ipc.Close() return nil, nil, err } return &process{ command: cmd, }, ipc, nil } func (p *process) Wait() error { return p.command.Wait() } func inheritedProcessIPC() (io.ReadWriteCloser, error) { fd0, err := strconv.ParseUint(os.Getenv("MM_IPC_FD0"), 0, 64) if err != nil { return nil, pkgerrors.Wrapf(err, "unable to get ipc file descriptor 0") } fd1, err := strconv.ParseUint(os.Getenv("MM_IPC_FD1"), 0, 64) if err != nil { return nil, pkgerrors.Wrapf(err, "unable to get ipc file descriptor 1") } return InheritedIPC(uintptr(fd0), uintptr(fd1)) } // XXX: EVERYTHING BELOW THIS IS COPIED / PASTED STANDARD LIBRARY CODE! // IT CAN BE DELETED IF / WHEN THIS ISSUE IS RESOLVED: https://github.com/golang/go/issues/21085 // Just about all of os/exec/exec.go is copied / pasted below, altered to use our modified startProcess functions even // further below. 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. // If Env is nil, Run uses the current process's environment. 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 Stdout and Stderr are the same writer, 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. // // BUG(rsc): On OS X 10.6, child processes may sometimes inherit unwanted fds. // https://golang.org/issue/2603 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{} } func command(name string, arg ...string) *cmd { cmd := &cmd{ Path: name, Args: append([]string{name}, arg...), } if filepath.Base(name) == name { if lp, err := exec.LookPath(name); err != nil { cmd.lookPathErr = err } else { cmd.Path = lp } } return cmd } 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 } func interfaceEqual(a, b interface{}) bool { defer func() { recover() }() return a == b } func (c *cmd) envv() []string { if c.Env != nil { return c.Env } return os.Environ() } func (c *cmd) argv() []string { if len(c.Args) > 0 { return c.Args } return []string{c.Path} } 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() } } func lookExtensions(path, dir string) (string, error) { if filepath.Base(path) == path { path = filepath.Join(".", path) } if dir == "" { return exec.LookPath(path) } if filepath.VolumeName(path) != "" { return exec.LookPath(path) } if len(path) > 1 && os.IsPathSeparator(path[0]) { return exec.LookPath(path) } dirandpath := filepath.Join(dir, path) // We assume that LookPath will only add file extension. lp, err := exec.LookPath(dirandpath) if err != nil { return "", err } ext := strings.TrimPrefix(lp, dirandpath) return path + ext, nil } // Copied from os/exec/exec.go, altered to use osStartProcess (defined below). 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: } } 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...) var err error c.Process, err = osStartProcess(c.Path, c.argv(), &os.ProcAttr{ Dir: c.Dir, Files: c.childFiles, Env: c.envv(), Sys: c.SysProcAttr, }) if err != nil { c.closeDescriptors(c.closeAfterStart) c.closeDescriptors(c.closeAfterWait) return err } c.closeDescriptors(c.closeAfterStart) 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 } 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 &exec.ExitError{ProcessState: state} } return copyError } // Copied from os/exec_posix.go, altered to use syscallStartProcess (defined below). func osStartProcess(name string, argv []string, attr *os.ProcAttr) (p *os.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 := os.Stat(attr.Dir); err != nil { pe := err.(*os.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 = os.Environ() } for _, f := range attr.Files { sysattr.Files = append(sysattr.Files, f.Fd()) } pid, _, e := syscallStartProcess(name, argv, sysattr) if e != nil { return nil, &os.PathError{Op: "fork/exec", Path: name, Err: e} } return os.FindProcess(pid) } // Everything from this point on is copied from syscall/exec_windows.go func makeCmdLine(args []string) string { var s string for _, v := range args { if s != "" { s += " " } s += syscall.EscapeArg(v) } return s } 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 isSlash(c uint8) bool { return c == '\\' || c == '/' } func normalizeDir(dir string) (name string, err error) { ndir, err := syscall.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 "", syscall.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 "", syscall.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 "", syscall.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 syscall.FullPath(d + "\\" + p[2:]) } else { return syscall.FullPath(p) } } } else { // no drive letter d, err := normalizeDir(dir) if err != nil { return "", err } if isSlash(p[0]) { return syscall.FullPath(d[:2] + p) } else { return syscall.FullPath(d + "\\" + p) } } } var zeroProcAttr syscall.ProcAttr var zeroSysProcAttr syscall.SysProcAttr // Has minor changes to support file inheritance. func syscallStartProcess(argv0 string, argv []string, attr *syscall.ProcAttr) (pid int, handle uintptr, err error) { if len(argv0) == 0 { return 0, 0, syscall.EWINDOWS } if attr == nil { attr = &zeroProcAttr } sys := attr.Sys if sys == nil { sys = &zeroSysProcAttr } if len(attr.Files) < 3 { return 0, 0, syscall.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 := syscall.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 = syscall.UTF16PtrFromString(cmdline) if err != nil { return 0, 0, err } } var dirp *uint16 if len(attr.Dir) != 0 { dirp, err = syscall.UTF16PtrFromString(attr.Dir) if err != nil { return 0, 0, err } } // Acquire the fork lock so that no other threads // create new fds that are not yet close-on-exec // before we fork. syscall.ForkLock.Lock() defer syscall.ForkLock.Unlock() p, _ := syscall.GetCurrentProcess() fd := make([]syscall.Handle, len(attr.Files)) for i := range attr.Files { if attr.Files[i] <= 0 { continue } if i < 3 { err := syscall.DuplicateHandle(p, syscall.Handle(attr.Files[i]), p, &fd[i], 0, true, syscall.DUPLICATE_SAME_ACCESS) if err != nil { return 0, 0, err } defer syscall.CloseHandle(syscall.Handle(fd[i])) } else { // This is the modification that allows files to be inherited. syscall.SetHandleInformation(syscall.Handle(attr.Files[i]), syscall.HANDLE_FLAG_INHERIT, 1) defer syscall.SetHandleInformation(syscall.Handle(attr.Files[i]), syscall.HANDLE_FLAG_INHERIT, 0) } } si := new(syscall.StartupInfo) si.Cb = uint32(unsafe.Sizeof(*si)) si.Flags = syscall.STARTF_USESTDHANDLES if sys.HideWindow { si.Flags |= syscall.STARTF_USESHOWWINDOW si.ShowWindow = syscall.SW_HIDE } si.StdInput = fd[0] si.StdOutput = fd[1] si.StdErr = fd[2] pi := new(syscall.ProcessInformation) flags := sys.CreationFlags | syscall.CREATE_UNICODE_ENVIRONMENT err = syscall.CreateProcess(argv0p, argvp, nil, nil, true, flags, createEnvBlock(attr.Env), dirp, si, pi) if err != nil { return 0, 0, err } defer syscall.CloseHandle(syscall.Handle(pi.Thread)) return int(pi.ProcessId), uintptr(pi.Process), nil }