-
Notifications
You must be signed in to change notification settings - Fork 256
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add new exec package for host process containers
This change adds a new exec package thats main goal is to run external processes on Windows. Unfortunately due to a couple things that can't be accomplished with the stdlib os/exec package, this new package is meant to replace how processes for host process containers are launched. The main shortcomings are not being able to pass in a pseudo console to use for tty scenarios, and not being able to start a process assigned to a job object instead of doing the Create -> Assign dance. Both of these issue are centered around not having access to the process thread attribute list that is setup inside of syscall.StartProcess. This is needed to be able to properly setup both cases, as it requires calling UpdateProcThreadAttribute and passing in what's necessary for both scenarios. Signed-off-by: Daniel Canter <dcanter@microsoft.com>
- Loading branch information
Showing
8 changed files
with
979 additions
and
20 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
package exec | ||
|
||
import ( | ||
"context" | ||
"io" | ||
"io/ioutil" | ||
"log" | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/Microsoft/hcsshim/internal/jobobject" | ||
) | ||
|
||
func TestExec(t *testing.T) { | ||
// Exec a simple process and wait for exit. | ||
e, err := New( | ||
`C:\Windows\System32\ping.exe`, | ||
"ping 127.0.0.1", | ||
WithEnv(os.Environ()), | ||
) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
err = e.Start() | ||
if err != nil { | ||
t.Fatalf("failed to start process: %v", err) | ||
} | ||
|
||
err = e.Wait() | ||
if err != nil { | ||
t.Fatalf("error waiting for process: %v", err) | ||
} | ||
t.Logf("exit code was: %d", e.ExitCode()) | ||
} | ||
|
||
func TestExecWithDir(t *testing.T) { | ||
// Test that the working directory is successfully set to whatever was passed in. | ||
dir, err := ioutil.TempDir("", "exec-test") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
defer os.RemoveAll(dir) | ||
|
||
e, err := New( | ||
`C:\Windows\System32\cmd.exe`, | ||
"cmd /c echo 'test' > test.txt", | ||
WithDir(dir), | ||
WithEnv(os.Environ()), | ||
) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
err = e.Start() | ||
if err != nil { | ||
t.Fatalf("failed to start process: %v", err) | ||
} | ||
|
||
err = e.Wait() | ||
if err != nil { | ||
t.Fatalf("error waiting for process: %v", err) | ||
} | ||
|
||
if _, err := os.Stat(filepath.Join(dir, "test.txt")); err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
t.Logf("exit code was: %d", e.ExitCode()) | ||
} | ||
|
||
func TestExecStdinPowershell(t *testing.T) { | ||
// Exec a powershel 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", | ||
WithStdio(true, false, true), | ||
WithEnv(os.Environ()), | ||
) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
err = e.Start() | ||
if err != nil { | ||
t.Fatalf("failed to start process: %v", err) | ||
} | ||
|
||
stdinChan := make(chan error) | ||
go func() { | ||
_, _ = io.Copy(os.Stdout, e.Stdout()) | ||
}() | ||
|
||
go func() { | ||
cmd := `ping 127.0.0.1 | ||
` | ||
|
||
exit := `exit | ||
` | ||
if _, err := e.Stdin().Write([]byte(cmd)); err != nil { | ||
stdinChan <- err | ||
} | ||
if _, err := e.Stdin().Write([]byte(exit)); err != nil { | ||
stdinChan <- err | ||
} | ||
close(stdinChan) | ||
}() | ||
|
||
err = <-stdinChan | ||
if 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()) | ||
} | ||
|
||
func TestExecWithJob(t *testing.T) { | ||
// Test that we can assign a process to a job object at creation time. | ||
job, err := jobobject.Create(context.Background(), &jobobject.Options{Name: "test"}) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
defer job.Close() | ||
|
||
e, err := New( | ||
`C:\Windows\System32\ping.exe`, | ||
"ping 127.0.0.1", | ||
WithJobObject(job), | ||
WithStdio(true, false, false), | ||
WithEnv(os.Environ()), | ||
) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
err = e.Start() | ||
if err != nil { | ||
t.Fatalf("failed to start process: %v", err) | ||
} | ||
|
||
pids, err := job.Pids() | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
// Should only be one process in the job | ||
if pids[0] != uint32(e.Pid()) { | ||
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()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package exec | ||
|
||
import ( | ||
"github.com/Microsoft/hcsshim/internal/jobobject" | ||
"golang.org/x/sys/windows" | ||
) | ||
|
||
type ExecOpts func(e *execConfig) error | ||
|
||
type execConfig struct { | ||
dir string | ||
env []string | ||
stdout, stderr, stdin bool | ||
|
||
job *jobobject.JobObject | ||
token windows.Token | ||
processFlags uint32 | ||
} | ||
|
||
// WithDir will use `dir` as the working directory for the process. | ||
func WithDir(dir string) ExecOpts { | ||
return func(e *execConfig) error { | ||
e.dir = dir | ||
return nil | ||
} | ||
} | ||
|
||
// WithStdio will hook up stdio for the process to a pipe, the other end of which can be retrieved by calling Stdout(), stdErr(), or Stdin() | ||
// respectively on the Exec object. Stdio will be hooked up to the NUL device otherwise. | ||
func WithStdio(stdout, stderr, stdin bool) ExecOpts { | ||
return func(e *execConfig) error { | ||
e.stdout = stdout | ||
e.stderr = stderr | ||
e.stdin = stdin | ||
return nil | ||
} | ||
} | ||
|
||
// WithEnv will use the passed in environment variables for the new process. | ||
func WithEnv(env []string) ExecOpts { | ||
return func(e *execConfig) error { | ||
e.env = env | ||
return nil | ||
} | ||
} | ||
|
||
// WithJobObject will launch the newly created process in the passed in job. | ||
func WithJobObject(job *jobobject.JobObject) ExecOpts { | ||
return func(e *execConfig) error { | ||
e.job = job | ||
return nil | ||
} | ||
} | ||
|
||
// WithToken will run the process as the user that `token` represents. | ||
func WithToken(token windows.Token) ExecOpts { | ||
return func(e *execConfig) error { | ||
e.token = token | ||
return nil | ||
} | ||
} | ||
|
||
// WithProcessFlags will pass `flags` to CreateProcess's creationFlags parameter. | ||
func WithProcessFlags(flags uint32) ExecOpts { | ||
return func(e *execConfig) error { | ||
e.processFlags = flags | ||
return nil | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,80 @@ | ||
package winapi | ||
|
||
import ( | ||
"errors" | ||
"unsafe" | ||
|
||
"golang.org/x/sys/windows" | ||
) | ||
|
||
const PROCESS_ALL_ACCESS uint32 = 2097151 | ||
|
||
// DWORD GetProcessImageFileNameW( | ||
// HANDLE hProcess, | ||
// LPWSTR lpImageFileName, | ||
// DWORD nSize | ||
const ( | ||
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x20016 | ||
PROC_THREAD_ATTRIBUTE_JOB_LIST = 0x2000D | ||
) | ||
|
||
type ProcThreadAttributeList struct { | ||
_ [1]byte | ||
} | ||
|
||
// typedef struct _STARTUPINFOEXW { | ||
// STARTUPINFOW StartupInfo; | ||
// LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList; | ||
// } STARTUPINFOEXW, *LPSTARTUPINFOEXW; | ||
type StartupInfoEx struct { | ||
// This is a recreation of the same binding from the stdlib. The x/sys/windows variant for whatever reason | ||
// doesn't work when updating the list for the pseudo console attribute. It has the process immediately exit | ||
// with exit code 0xc0000142 shortly after start. | ||
windows.StartupInfo | ||
ProcThreadAttributeList *ProcThreadAttributeList | ||
} | ||
|
||
// NewProcThreadAttributeList allocates a new ProcThreadAttributeList, with | ||
// the requested maximum number of attributes. This must be cleaned up by calling | ||
// DeleteProcThreadAttributeList. | ||
func NewProcThreadAttributeList(maxAttrCount uint32) (*ProcThreadAttributeList, error) { | ||
var size uintptr | ||
err := InitializeProcThreadAttributeList(nil, maxAttrCount, 0, &size) | ||
if err != windows.ERROR_INSUFFICIENT_BUFFER { | ||
if err == nil { | ||
return nil, errors.New("unable to query buffer size from InitializeProcThreadAttributeList") | ||
} | ||
return nil, err | ||
} | ||
al := (*ProcThreadAttributeList)(unsafe.Pointer(&make([]byte, size)[0])) | ||
err = InitializeProcThreadAttributeList(al, maxAttrCount, 0, &size) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return al, nil | ||
} | ||
|
||
// BOOL InitializeProcThreadAttributeList( | ||
// [out, optional] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList, | ||
// [in] DWORD dwAttributeCount, | ||
// DWORD dwFlags, | ||
// [in, out] PSIZE_T lpSize | ||
// ); | ||
//sys GetProcessImageFileName(hProcess windows.Handle, imageFileName *uint16, nSize uint32) (size uint32, err error) = kernel32.GetProcessImageFileNameW | ||
// | ||
//sys InitializeProcThreadAttributeList(lpAttributeList *ProcThreadAttributeList, dwAttributeCount uint32, dwFlags uint32, lpSize *uintptr) (err error) = kernel32.InitializeProcThreadAttributeList | ||
|
||
// void DeleteProcThreadAttributeList( | ||
// [in, out] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList | ||
// ); | ||
// | ||
//sys DeleteProcThreadAttributeList(lpAttributeList *ProcThreadAttributeList) = kernel32.DeleteProcThreadAttributeList | ||
|
||
// BOOL UpdateProcThreadAttribute( | ||
// [in, out] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList, | ||
// [in] DWORD dwFlags, | ||
// [in] DWORD_PTR Attribute, | ||
// [in] PVOID lpValue, | ||
// [in] SIZE_T cbSize, | ||
// [out, optional] PVOID lpPreviousValue, | ||
// [in, optional] PSIZE_T lpReturnSize | ||
// ); | ||
// | ||
//sys UpdateProcThreadAttribute(lpAttributeList *ProcThreadAttributeList, dwFlags uint32, attribute uintptr, lpValue unsafe.Pointer, cbSize uintptr, lpPreviousValue unsafe.Pointer, lpReturnSize *uintptr) (err error) = kernel32.UpdateProcThreadAttribute | ||
|
||
//sys CreateProcessAsUser(token windows.Token, appName *uint16, commandLine *uint16, procSecurity *windows.SecurityAttributes, threadSecurity *windows.SecurityAttributes, inheritHandles bool, creationFlags uint32, env *uint16, currentDir *uint16, startupInfo *windows.StartupInfo, outProcInfo *windows.ProcessInformation) (err error) = advapi32.CreateProcessAsUserW |
Oops, something went wrong.