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 }
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 }