Skip to content

Commit

Permalink
Add pseudo console support to exec package
Browse files Browse the repository at this point in the history
This change adds pseudo console support to the exec package.

Signed-off-by: Daniel Canter <dcanter@microsoft.com>
  • Loading branch information
dcantah committed Dec 16, 2021
1 parent 46e016f commit 6861cd6
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 7 deletions.
44 changes: 43 additions & 1 deletion internal/exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,15 @@ func (e *Exec) Start() error {
// 1. Assigning to a job object at creation time
// 2. Pseudo console setup if one was requested.
// 3. Inherit only stdio handles if ones were requested.
// Therefore we need a list of size 3.
e.attrList, err = winapi.NewProcThreadAttributeList(3)
if err != nil {
return fmt.Errorf("failed to initialize process thread attribute list: %w", err)
}
siEx.ProcThreadAttributeList = e.attrList

// Need to know whether the process needs to inherit stdio handles.
// Need to know whether the process needs to inherit stdio handles. The below setup is so that we only inherit the
// stdio pipes and nothing else into the new process.
inheritHandles := e.stdioProcEnd[0] != nil || e.stdioProcEnd[1] != nil || e.stdioProcEnd[2] != nil
if inheritHandles {
var handles []uintptr
Expand Down Expand Up @@ -153,6 +155,12 @@ func (e *Exec) Start() error {
}
}

if e.cpty != nil {
if err := e.cpty.UpdateProcThreadAttribute(siEx.ProcThreadAttributeList); err != nil {
return err
}
}

var zeroSec windows.SecurityAttributes
pSec := &windows.SecurityAttributes{Length: uint32(unsafe.Sizeof(zeroSec)), InheritHandle: 1}
tSec := &windows.SecurityAttributes{Length: uint32(unsafe.Sizeof(zeroSec)), InheritHandle: 1}
Expand Down Expand Up @@ -294,12 +302,30 @@ func (e *Exec) Stderr() *os.File {

// setupStdio handles setting up stdio for the process.
func (e *Exec) setupStdio() error {
stdioRequested := e.stdin || e.stderr || e.stdout
// If the client requested a pseudo console then there's nothing we need to do pipe wise, as the process inherits the other end of the pty's
// pipes.
if e.cpty != nil && stdioRequested {
return errors.New("can't setup both stdio pipes and a pseudo console")
}

// Go 1.16's pipe handles (from os.Pipe()) aren't inheritable, so mark them explicitly as such if any stdio handles are
// requested and someone may be building on 1.16.

if e.stdin {
pr, pw, err := os.Pipe()
if err != nil {
return err
}
e.stdioOurEnd[0] = pw

if err := windows.SetHandleInformation(
windows.Handle(pr.Fd()),
windows.HANDLE_FLAG_INHERIT,
windows.HANDLE_FLAG_INHERIT,
); err != nil {
return fmt.Errorf("failed to make stdin pipe inheritable: %w", err)
}
e.stdioProcEnd[0] = pr
}

Expand All @@ -309,6 +335,14 @@ func (e *Exec) setupStdio() error {
return err
}
e.stdioOurEnd[1] = pr

if err := windows.SetHandleInformation(
windows.Handle(pw.Fd()),
windows.HANDLE_FLAG_INHERIT,
windows.HANDLE_FLAG_INHERIT,
); err != nil {
return fmt.Errorf("failed to make stdout pipe inheritable: %w", err)
}
e.stdioProcEnd[1] = pw
}

Expand All @@ -318,6 +352,14 @@ func (e *Exec) setupStdio() error {
return err
}
e.stdioOurEnd[2] = pr

if err := windows.SetHandleInformation(
windows.Handle(pw.Fd()),
windows.HANDLE_FLAG_INHERIT,
windows.HANDLE_FLAG_INHERIT,
); err != nil {
return fmt.Errorf("failed to make stderr pipe inheritable: %w", err)
}
e.stdioProcEnd[2] = pw
}
return nil
Expand Down
84 changes: 78 additions & 6 deletions internal/exec/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import (
"log"
"os"
"path/filepath"
"strings"
"testing"
"time"

"github.com/Microsoft/hcsshim/internal/conpty"
"github.com/Microsoft/hcsshim/internal/jobobject"
)

Expand Down Expand Up @@ -71,7 +74,7 @@ func TestExecWithDir(t *testing.T) {
}

func TestExecStdinPowershell(t *testing.T) {
// Exec a powershel instance and test that we can write commands to stdin and receive the output from stdout.
// Exec a powershell instance and test that we can write commands to stdin and receive the output from stdout.
e, err := New(
`C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`,
"powershell",
Expand All @@ -87,7 +90,7 @@ func TestExecStdinPowershell(t *testing.T) {
t.Fatalf("failed to start process: %v", err)
}

stdinChan := make(chan error)
errChan := make(chan error)
go func() {
_, _ = io.Copy(os.Stdout, e.Stdout())
}()
Expand All @@ -99,15 +102,15 @@ func TestExecStdinPowershell(t *testing.T) {
exit := `exit
`
if _, err := e.Stdin().Write([]byte(cmd)); err != nil {
stdinChan <- err
errChan <- err
}
if _, err := e.Stdin().Write([]byte(exit)); err != nil {
stdinChan <- err
errChan <- err
}
close(stdinChan)
close(errChan)
}()

err = <-stdinChan
err = <-errChan
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -159,3 +162,72 @@ func TestExecWithJob(t *testing.T) {
}
t.Logf("exit code was: %d", e.ExitCode())
}

func TestPseudoConsolePowershell(t *testing.T) {
cpty, err := conpty.New(80, 20, 0)
if err != nil {
t.Fatal(err)
}
defer cpty.Close()

// Exec a powershell instance and test that we can write commands to the input side of the pty and receive data
// from the output end.
e, err := New(
`C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`,
"powershell",
WithEnv(os.Environ()),
WithConPty(cpty),
)
if err != nil {
t.Fatal(err)
}

err = e.Start()
if err != nil {
t.Fatalf("failed to start process: %v", err)
}

errChan := make(chan error)
go func() {
buf := make([]byte, 1000)
for {
_, err := cpty.Read(buf)
if err != nil {
errChan <- err
}

if !strings.Contains(string(buf), "howdy from conpty") {
continue
}
close(errChan)
break
}
}()

cmd := "echo \"howdy from conpty\"\r\n"
if _, err := cpty.InPipe().Write([]byte(cmd)); err != nil {
t.Fatal(err)
}

// If after five seconds we haven't read the output we wrote to the pseudo console below then
// fail the test.
select {
case <-time.After(time.Second * 5):
t.Fatal("timed out waiting for output to pseudo console")
case err := <-errChan:
if err != nil {
t.Fatal(err)
}
}

exit := "exit\r\n"
if _, err := cpty.InPipe().Write([]byte(exit)); err != nil {
t.Fatal(err)
}

err = e.Wait()
if err != nil {
t.Fatalf("error waiting for process: %v", err)
}
t.Logf("exit code was: %d", e.ExitCode())
}
10 changes: 10 additions & 0 deletions internal/exec/options.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package exec

import (
"github.com/Microsoft/hcsshim/internal/conpty"
"github.com/Microsoft/hcsshim/internal/jobobject"
"golang.org/x/sys/windows"
)
Expand All @@ -13,6 +14,7 @@ type execConfig struct {
stdout, stderr, stdin bool

job *jobobject.JobObject
cpty *conpty.ConPTY
token windows.Token
processFlags uint32
}
Expand Down Expand Up @@ -52,6 +54,14 @@ func WithJobObject(job *jobobject.JobObject) ExecOpts {
}
}

// WithConPty will launch the created process with a pseudo console attached to the process.
func WithConPty(cpty *conpty.ConPTY) ExecOpts {
return func(e *execConfig) error {
e.cpty = cpty
return nil
}
}

// WithToken will run the process as the user that `token` represents.
func WithToken(token windows.Token) ExecOpts {
return func(e *execConfig) error {
Expand Down

0 comments on commit 6861cd6

Please sign in to comment.