mirror of
https://github.com/mattermost/mattermost.git
synced 2026-02-20 16:31:10 -05:00
648 lines
15 KiB
Go
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
|
|
}
|