mattermost/plugin/rpcplugin/process_windows.go
Chris f720288c10 windows support for plugin ipc (#7251)
* windows support for plugin ipc

* unix test fix
2017-08-18 12:21:01 -07:00

648 lines
15 KiB
Go

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
}