From 6861cd679a8d107791c5b4e415e8fbaa642533fb Mon Sep 17 00:00:00 2001 From: Daniel Canter Date: Thu, 16 Dec 2021 08:00:09 -0500 Subject: [PATCH] Add pseudo console support to exec package This change adds pseudo console support to the exec package. Signed-off-by: Daniel Canter --- internal/exec/exec.go | 44 +++++++++++++++++++- internal/exec/exec_test.go | 84 +++++++++++++++++++++++++++++++++++--- internal/exec/options.go | 10 +++++ 3 files changed, 131 insertions(+), 7 deletions(-) diff --git a/internal/exec/exec.go b/internal/exec/exec.go index 7bc588bba7..a6c69f8a9a 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -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 @@ -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} @@ -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 } @@ -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 } @@ -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 diff --git a/internal/exec/exec_test.go b/internal/exec/exec_test.go index 53423e31c7..6e0777a85e 100644 --- a/internal/exec/exec_test.go +++ b/internal/exec/exec_test.go @@ -7,8 +7,11 @@ import ( "log" "os" "path/filepath" + "strings" "testing" + "time" + "github.com/Microsoft/hcsshim/internal/conpty" "github.com/Microsoft/hcsshim/internal/jobobject" ) @@ -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", @@ -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()) }() @@ -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) } @@ -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()) +} diff --git a/internal/exec/options.go b/internal/exec/options.go index d7a9ac8d60..1811f65a91 100644 --- a/internal/exec/options.go +++ b/internal/exec/options.go @@ -1,6 +1,7 @@ package exec import ( + "github.com/Microsoft/hcsshim/internal/conpty" "github.com/Microsoft/hcsshim/internal/jobobject" "golang.org/x/sys/windows" ) @@ -13,6 +14,7 @@ type execConfig struct { stdout, stderr, stdin bool job *jobobject.JobObject + cpty *conpty.ConPTY token windows.Token processFlags uint32 } @@ -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 {