예제 #1
0
func newExecShell(command []string, tty bool) (engines.Shell, error) {
	if len(command) == 0 {
		command = []string{defaultShell}
	}
	s := &execShell{
		cmd: exec.Command(command[0], command[1:]...),
	}

	// Start is wrapped in pty, if shell is supposed to emulate a TTY
	var err error
	if tty && pty.Supported {
		s.pty, err = pty.Start(s.cmd)
		if err != nil {
			// if there was a start error we set empty streams
			s.stdin = ioext.WriteNopCloser(ioutil.Discard)
			s.stdout = ioutil.NopCloser(bytes.NewReader(nil))
			s.stderr = ioutil.NopCloser(bytes.NewReader(nil))
		} else {
			s.stdin = s.pty
			s.stdout = s.pty
			s.stderr = ioutil.NopCloser(bytes.NewReader(nil))
		}
	} else {
		s.cmd.Stdin, s.stdin = io.Pipe()
		s.stdout, s.cmd.Stdout = io.Pipe()
		s.stderr, s.cmd.Stderr = io.Pipe()

		err = s.cmd.Start()
	}

	// if there was an error starting, then we just resolve as is... Hence, it'll
	// be empty stdio and false result.
	if err != nil {
		s.resolve.Do(func() {
			s.stdin.Close()
			s.stdout.Close()
			s.stderr.Close()

			s.result = false
			s.abortErr = engines.ErrShellTerminated
		})
	} else {
		// otherwise wait for the result, and resolve when shell terminates
		go s.waitForResult()
	}

	return s, nil
}
예제 #2
0
func TestSystem(t *testing.T) {
	// Setup temporary home directory
	homeDir := filepath.Join(os.TempDir(), slugid.Nice())
	require.NoError(t, os.MkdirAll(homeDir, 0777))
	defer os.RemoveAll(homeDir)

	var u *User
	var err error

	t.Run("CreateUser", func(t *testing.T) {
		u, err = CreateUser(homeDir, nil)
		require.NoError(t, err)
	})

	t.Run("FindGroup", func(t *testing.T) {
		g, err := FindGroup(testGroup)
		require.NoError(t, err)
		require.NotNil(t, g)
	})

	t.Run("StartProcess True", func(t *testing.T) {
		p, err := StartProcess(ProcessOptions{
			Arguments: testTrue,
		})
		require.NoError(t, err)
		require.True(t, p.Wait())
	})

	t.Run("StartProcess False", func(t *testing.T) {
		p, err := StartProcess(ProcessOptions{
			Arguments: testFalse,
		})
		require.NoError(t, err)
		require.False(t, p.Wait())
	})

	t.Run("StartProcess True with TTY", func(t *testing.T) {
		p, err := StartProcess(ProcessOptions{
			Arguments: testTrue,
			TTY:       true,
		})
		require.NoError(t, err)
		require.True(t, p.Wait())
	})

	t.Run("StartProcess False with TTY", func(t *testing.T) {
		p, err := StartProcess(ProcessOptions{
			Arguments: testFalse,
			TTY:       true,
		})
		require.NoError(t, err)
		require.False(t, p.Wait())
	})

	t.Run("StartProcess Cat", func(t *testing.T) {
		var out bytes.Buffer
		p, err := StartProcess(ProcessOptions{
			Arguments: testCat,
			Stdin:     ioutil.NopCloser(bytes.NewBufferString("hello-world")),
			Stdout:    ioext.WriteNopCloser(&out),
		})
		require.NoError(t, err)
		require.True(t, p.Wait())
		require.Equal(t, "hello-world", out.String())
	})

	t.Run("StartProcess Cat with TTY", func(t *testing.T) {
		p, err := StartProcess(ProcessOptions{
			Arguments: testCat,
			TTY:       true,
			Stdin:     ioutil.NopCloser(bytes.NewBufferString("hello-world")),
			// We can't reliably read output as we kill the process with stdin
			// is closed (EOF)
		})
		require.NoError(t, err)
		assert.False(t, p.Wait())
	})

	t.Run("StartProcess Print Dir", func(t *testing.T) {
		var out bytes.Buffer
		p, err := StartProcess(ProcessOptions{
			Arguments:     testPrintDir,
			Stdout:        ioext.WriteNopCloser(&out),
			WorkingFolder: homeDir,
		})
		require.NoError(t, err)
		require.True(t, p.Wait())
		require.Contains(t, out.String(), homeDir)
	})

	t.Run("StartProcess TTY Print Dir", func(t *testing.T) {
		var out bytes.Buffer
		p, err := StartProcess(ProcessOptions{
			Arguments:     testPrintDir,
			Stdout:        ioext.WriteNopCloser(&out),
			WorkingFolder: homeDir,
			TTY:           true,
		})
		require.NoError(t, err)
		require.True(t, p.Wait())
		require.Contains(t, out.String(), homeDir)
	})

	t.Run("StartProcess Owner and Print Dir", func(t *testing.T) {
		var out bytes.Buffer
		p, err := StartProcess(ProcessOptions{
			Arguments: testPrintDir,
			Stdout:    ioext.WriteNopCloser(&out),
			Owner:     u,
		})
		require.NoError(t, err)
		require.True(t, p.Wait())
		require.Contains(t, out.String(), homeDir)
	})

	t.Run("StartProcess TTY, Owner and Print Dir", func(t *testing.T) {
		var out bytes.Buffer
		p, err := StartProcess(ProcessOptions{
			Arguments: testPrintDir,
			Stdout:    ioext.WriteNopCloser(&out),
			Owner:     u,
			TTY:       true,
		})
		require.NoError(t, err)
		require.True(t, p.Wait())
		require.Contains(t, out.String(), homeDir)
	})

	t.Run("StartProcess Owner and True", func(t *testing.T) {
		p, err := StartProcess(ProcessOptions{
			Arguments: testTrue,
			Owner:     u,
		})
		require.NoError(t, err)
		require.True(t, p.Wait())
	})

	t.Run("StartProcess TTY, Owner and True", func(t *testing.T) {
		p, err := StartProcess(ProcessOptions{
			Arguments: testTrue,
			Owner:     u,
			TTY:       true,
		})
		require.NoError(t, err)
		require.True(t, p.Wait())
	})

	t.Run("StartProcess Owner and False", func(t *testing.T) {
		p, err := StartProcess(ProcessOptions{
			Arguments: testFalse,
			Owner:     u,
		})
		require.NoError(t, err)
		require.False(t, p.Wait())
	})

	t.Run("StartProcess TTY, Owner and False", func(t *testing.T) {
		p, err := StartProcess(ProcessOptions{
			Arguments: testFalse,
			Owner:     u,
			TTY:       true,
		})
		require.NoError(t, err)
		require.False(t, p.Wait())
	})

	t.Run("StartProcess Kill", func(t *testing.T) {
		p, err := StartProcess(ProcessOptions{
			Arguments: testSleep,
		})
		require.NoError(t, err)
		var waited atomics.Bool
		done := make(chan bool)
		go func() {
			p.Wait()
			done <- waited.Get()
		}()
		time.Sleep(100 * time.Millisecond)
		waited.Set(true)
		p.Kill()
		require.False(t, p.Wait())
		require.True(t, <-done, "p.Wait was done before p.Kill() was called!")
	})

	t.Run("KillByOwner", func(t *testing.T) {
		p, err := StartProcess(ProcessOptions{
			Arguments: testSleep,
			Owner:     u,
		})
		require.NoError(t, err)
		var waited atomics.Bool
		done := make(chan bool)
		go func() {
			p.Wait()
			done <- waited.Get()
		}()
		time.Sleep(100 * time.Millisecond)
		waited.Set(true)
		require.NoError(t, KillByOwner(u))
		require.True(t, <-done, "p.Wait was done before KillByOwner was called!")
	})

	t.Run("user.Remove", func(t *testing.T) {
		u.Remove()
	})
}
// StartProcess starts a new process with given arguments, environment variables,
// and current working folder, running as given user.
//
// Returns an human readable error explaining why the sub-process couldn't start
// if not successful.
func StartProcess(options ProcessOptions) (*Process, error) {
	// Default arguments to system shell
	if len(options.Arguments) == 0 {
		options.Arguments = []string{defaultShell}
	}

	// If WorkingFolder isn't set find home folder of options.Owner (if set)
	// or current user
	if options.WorkingFolder == "" {
		if options.Owner != nil {
			options.WorkingFolder = options.Owner.homeFolder
		} else {
			u, err := user.Current()
			if err != nil {
				panic(fmt.Sprintf("Failed to lookup current user, error: %s", err))
			}
			options.WorkingFolder = u.HomeDir
		}
	}

	// Default stdout to os.DevNul
	if options.Stdout == nil {
		options.Stdout = ioext.WriteNopCloser(ioutil.Discard)
	}

	// Default stderr to stdout
	if options.Stderr == nil {
		options.Stderr = options.Stdout
	}

	// Create process and command
	p := &Process{}
	p.cmd = exec.Command(options.Arguments[0], options.Arguments[1:]...)
	p.cmd.Env = formatEnv(options.Environment)
	p.cmd.Dir = options.WorkingFolder

	// Set owner for the process
	if options.Owner != nil {
		p.cmd.SysProcAttr = &syscall.SysProcAttr{
			Credential: &syscall.Credential{
				Uid: options.Owner.uid,
				Gid: options.Owner.gid,
			},
		}
	}

	// Start the process
	var err error
	if !options.TTY {
		p.cmd.Stdin = options.Stdin
		p.cmd.Stdout = options.Stdout
		p.cmd.Stderr = options.Stderr
		p.stdin = options.Stdin
		p.stdout = options.Stdout
		p.stderr = options.Stderr
		err = p.cmd.Start()
	} else {
		p.pty, err = pty.Start(p.cmd)
		if options.Stdin != nil {
			p.sockets.Add(1)
			go func() {
				io.Copy(p.pty, options.Stdin)
				p.sockets.Done()
				// Kill process when stdin ends (if running as TTY)
				p.Kill()
			}()
		}
		p.sockets.Add(1)
		go func() {
			ioext.CopyAndClose(options.Stdout, p.pty)
			p.sockets.Done()
		}()
	}

	if err != nil {
		return nil, fmt.Errorf("Unable to execute binary, error: %s", err)
	}

	// Go wait for result
	go p.waitForResult()

	return p, nil
}