From 7556251ab64e43d93f81283c8e7ce0e4ba7d7ddc Mon Sep 17 00:00:00 2001 From: "Justin Terry (VM)" Date: Mon, 31 Jul 2017 12:31:11 -0700 Subject: [PATCH] Adds console resize support. Resolves: #76 --- service/gcs/bridge/bridge.go | 10 +-- service/gcs/core/core.go | 5 +- service/gcs/core/gcs/gcs.go | 142 +++++++++++++++++++++---------- service/gcs/runtime/runc/runc.go | 19 +++-- service/gcs/runtime/runtime.go | 2 + service/gcs/stdio/tty.go | 18 ++++ 6 files changed, 141 insertions(+), 55 deletions(-) diff --git a/service/gcs/bridge/bridge.go b/service/gcs/bridge/bridge.go index 40a7ac16..a02b786f 100644 --- a/service/gcs/bridge/bridge.go +++ b/service/gcs/bridge/bridge.go @@ -276,7 +276,7 @@ func (b *bridge) signalProcess(message []byte) (*prot.MessageResponseBase, error } response.ActivityID = request.ActivityID - if err := b.coreint.SignalProcess(int(request.ProcessID), request.Options); err != nil { + if err := b.coreint.SignalProcess(request.ContainerID, int(request.ProcessID), request.Options); err != nil { return response, err } @@ -348,15 +348,13 @@ func (b *bridge) waitOnProcess(message []byte, header *prot.MessageHeader) (*pro logrus.Error(errors.Wrapf(err, "failed to send process exit response \"%v\"", response)) } } - if err := b.coreint.RegisterProcessExitHook(int(request.ProcessID), exitHook); err != nil { + if err := b.coreint.RegisterProcessExitHook(request.ContainerID, int(request.ProcessID), exitHook); err != nil { return response, err } return response, nil } -// resizeConsole is currently a nop until the functionality is implemented. -// TODO: Tests still need to be written when it's no longer a nop. func (b *bridge) resizeConsole(message []byte) (*prot.MessageResponseBase, error) { response := newResponseBase() var request prot.ContainerResizeConsole @@ -365,7 +363,9 @@ func (b *bridge) resizeConsole(message []byte) (*prot.MessageResponseBase, error } response.ActivityID = request.ActivityID - // NOP + if err := b.coreint.ResizeConsole(request.ContainerID, int(request.ProcessID), request.Height, request.Width); err != nil { + return response, err + } return response, nil } diff --git a/service/gcs/core/core.go b/service/gcs/core/core.go index 2ebf58df..fe97e2b6 100644 --- a/service/gcs/core/core.go +++ b/service/gcs/core/core.go @@ -16,10 +16,11 @@ type Core interface { CreateContainer(id string, info prot.VMHostedContainerSettings) error ExecProcess(id string, info prot.ProcessParameters, stdioSet *stdio.ConnectionSet) (pid int, err error) SignalContainer(id string, signal oslayer.Signal) error - SignalProcess(pid int, options prot.SignalProcessOptions) error + SignalProcess(id string, pid int, options prot.SignalProcessOptions) error ListProcesses(id string) ([]runtime.ContainerProcessState, error) RunExternalProcess(info prot.ProcessParameters, stdioSet *stdio.ConnectionSet) (pid int, err error) ModifySettings(id string, request prot.ResourceModificationRequestResponse) error RegisterContainerExitHook(id string, onExit func(oslayer.ProcessExitState)) error - RegisterProcessExitHook(pid int, onExit func(oslayer.ProcessExitState)) error + RegisterProcessExitHook(id string, pid int, onExit func(oslayer.ProcessExitState)) error + ResizeConsole(id string, pid int, height, width uint16) error } diff --git a/service/gcs/core/gcs/gcs.go b/service/gcs/core/gcs/gcs.go index 7812b27e..18ff789d 100644 --- a/service/gcs/core/gcs/gcs.go +++ b/service/gcs/core/gcs/gcs.go @@ -37,12 +37,6 @@ type gcsCore struct { // ID to cache entry. containerCache map[string]*containerCacheEntry - processCacheMutex sync.RWMutex - // processCache stores information about processes which persists between - // calls into the gcsCore. It is structured as a map from pid to cache - // entry. - processCache map[int]*processCacheEntry - externalProcessCacheMutex sync.RWMutex // externalProcessCache stores information about external processes which // persists between calls into the gcsCore. It is structured as a map from @@ -56,7 +50,6 @@ func NewGCSCore(rtime runtime.Runtime, os oslayer.OS) *gcsCore { Rtime: rtime, OS: os, containerCache: make(map[string]*containerCacheEntry), - processCache: make(map[int]*processCacheEntry), externalProcessCache: make(map[int]*processCacheEntry), } } @@ -65,12 +58,17 @@ func NewGCSCore(rtime runtime.Runtime, os oslayer.OS) *gcsCore { type containerCacheEntry struct { ID string ExitStatus oslayer.ProcessExitState - Processes []int ExitHooks []func(oslayer.ProcessExitState) MappedVirtualDisks map[uint8]prot.MappedVirtualDisk MappedDirectories map[uint32]prot.MappedDirectory NetworkAdapters []prot.NetworkAdapter container runtime.Container + + processCacheMutex sync.RWMutex + // processCache stores information about processes which persists between + // calls into the gcsCore. It is structured as a map from pid to cache + // entry. + processCache map[int]*processCacheEntry } func newContainerCacheEntry(id string) *containerCacheEntry { @@ -78,14 +76,12 @@ func newContainerCacheEntry(id string) *containerCacheEntry { ID: id, MappedVirtualDisks: make(map[uint8]prot.MappedVirtualDisk), MappedDirectories: make(map[uint32]prot.MappedDirectory), + processCache: make(map[int]*processCacheEntry), } } func (e *containerCacheEntry) AddExitHook(hook func(oslayer.ProcessExitState)) { e.ExitHooks = append(e.ExitHooks, hook) } -func (e *containerCacheEntry) AddProcess(pid int) { - e.Processes = append(e.Processes, pid) -} func (e *containerCacheEntry) AddNetworkAdapter(adapter prot.NetworkAdapter) { e.NetworkAdapters = append(e.NetworkAdapters, adapter) } @@ -122,6 +118,7 @@ func (e *containerCacheEntry) RemoveMappedDirectory(dir prot.MappedDirectory) { type processCacheEntry struct { ExitStatus oslayer.ProcessExitState ExitHooks []func(oslayer.ProcessExitState) + Master *os.File } func newProcessCacheEntry() *processCacheEntry { @@ -207,7 +204,9 @@ func (c *gcsCore) ExecProcess(id string, params prot.ProcessParameters, stdioSet var p runtime.Process - isInitProcess := len(containerEntry.Processes) == 0 + containerEntry.processCacheMutex.Lock() + isInitProcess := len(containerEntry.processCache) == 0 + containerEntry.processCacheMutex.Unlock() if isInitProcess { if err := c.writeConfigFile(id, params.OCISpecification); err != nil { return -1, err @@ -220,6 +219,7 @@ func (c *gcsCore) ExecProcess(id string, params prot.ProcessParameters, stdioSet containerEntry.container = container p = container + processEntry.Master = p.Console() // Configure network adapters in the namespace. for _, adapter := range containerEntry.NetworkAdapters { @@ -244,17 +244,13 @@ func (c *gcsCore) ExecProcess(id string, params prot.ProcessParameters, stdioSet } c.containerCacheMutex.Unlock() - c.processCacheMutex.Lock() + containerEntry.processCacheMutex.Lock() processEntry.ExitStatus = state for _, hook := range processEntry.ExitHooks { hook(state) } - c.processCacheMutex.Unlock() + containerEntry.processCacheMutex.Unlock() c.containerCacheMutex.Lock() - containerEntry.ExitStatus = state - for _, hook := range containerEntry.ExitHooks { - hook(state) - } delete(c.containerCache, id) c.containerCacheMutex.Unlock() }() @@ -271,6 +267,7 @@ func (c *gcsCore) ExecProcess(id string, params prot.ProcessParameters, stdioSet if err != nil { return -1, err } + processEntry.Master = p.Console() go func() { state, err := p.Wait() @@ -279,19 +276,19 @@ func (c *gcsCore) ExecProcess(id string, params prot.ProcessParameters, stdioSet } logrus.Infof("container process %d exited with exit status %d", p.Pid(), state.ExitCode()) - c.processCacheMutex.Lock() + containerEntry.processCacheMutex.Lock() processEntry.ExitStatus = state for _, hook := range processEntry.ExitHooks { hook(state) } - c.processCacheMutex.Unlock() + containerEntry.processCacheMutex.Unlock() if err := p.Delete(); err != nil { logrus.Error(err) } }() } - c.processCacheMutex.Lock() + containerEntry.processCacheMutex.Lock() // If a processCacheEntry with the given pid already exists in the cache, // this will overwrite it. This behavior is expected. Processes are kept in // the cache even after they exit, which allows for exit hooks registered @@ -304,9 +301,8 @@ func (c *gcsCore) ExecProcess(id string, params prot.ProcessParameters, stdioSet // apply to the old process no longer makes sense, so since the old // process's pid has been reused, its cache entry can also be reused. This // applies to external processes as well. - c.processCache[p.Pid()] = processEntry - c.processCacheMutex.Unlock() - containerEntry.AddProcess(p.Pid()) + containerEntry.processCache[p.Pid()] = processEntry + containerEntry.processCacheMutex.Unlock() return p.Pid(), nil } @@ -330,18 +326,30 @@ func (c *gcsCore) SignalContainer(id string, signal oslayer.Signal) error { } // SignalProcess sends the signal specified in options to the given process. -func (c *gcsCore) SignalProcess(pid int, options prot.SignalProcessOptions) error { - c.processCacheMutex.Lock() - c.externalProcessCacheMutex.Lock() - if _, ok := c.processCache[pid]; !ok { +func (c *gcsCore) SignalProcess(id string, pid int, options prot.SignalProcessOptions) error { + if id == "" { + c.externalProcessCacheMutex.Lock() if _, ok := c.externalProcessCache[pid]; !ok { - c.processCacheMutex.Unlock() c.externalProcessCacheMutex.Unlock() return errors.WithStack(gcserr.NewProcessDoesNotExistError(pid)) } + c.externalProcessCacheMutex.Unlock() + } else { + c.containerCacheMutex.Lock() + containerEntry := c.getContainer(id) + if containerEntry == nil { + c.containerCacheMutex.Unlock() + return errors.WithStack(gcserr.NewProcessDoesNotExistError(pid)) + } + containerEntry.processCacheMutex.Lock() + if _, ok := containerEntry.processCache[pid]; !ok { + containerEntry.processCacheMutex.Unlock() + c.containerCacheMutex.Unlock() + return errors.WithStack(gcserr.NewProcessDoesNotExistError(pid)) + } + containerEntry.processCacheMutex.Unlock() + c.containerCacheMutex.Unlock() } - c.processCacheMutex.Unlock() - c.externalProcessCacheMutex.Unlock() // Interpret signal value 0 as SIGKILL. // TODO: Remove this special casing when we are not worried about breaking @@ -395,11 +403,11 @@ func (c *gcsCore) RunExternalProcess(params prot.ProcessParameters, stdioSet *st cmd.SetEnv(ociProcess.Env) var relay *stdio.TtyRelay + var master *os.File if params.EmulateConsole { // Allocate a console for the process. var ( consolePath string - master *os.File ) master, consolePath, err = stdio.NewConsole() if err != nil { @@ -441,6 +449,7 @@ func (c *gcsCore) RunExternalProcess(params prot.ProcessParameters, stdioSet *st } processEntry := newProcessCacheEntry() + processEntry.Master = master go func() { if err := cmd.Wait(); err != nil { // TODO: When cmd is a shell, and last command in the shell @@ -553,18 +562,28 @@ func (c *gcsCore) RegisterContainerExitHook(id string, exitHook func(oslayer.Pro // process may have multiple exit hooks registered for it. // This function works for both processes that are running in a container, and // ones that are running externally to a container. -func (c *gcsCore) RegisterProcessExitHook(pid int, exitHook func(oslayer.ProcessExitState)) error { - c.processCacheMutex.Lock() - defer c.processCacheMutex.Unlock() - c.externalProcessCacheMutex.Lock() - defer c.externalProcessCacheMutex.Unlock() +func (c *gcsCore) RegisterProcessExitHook(id string, pid int, exitHook func(oslayer.ProcessExitState)) error { + var ( + entry *processCacheEntry + ok bool + ) - var entry *processCacheEntry - var ok bool - entry, ok = c.processCache[pid] - if !ok { - entry, ok = c.externalProcessCache[pid] - if !ok { + if id == "" { + c.externalProcessCacheMutex.Lock() + defer c.externalProcessCacheMutex.Unlock() + if entry, ok = c.externalProcessCache[pid]; !ok { + return errors.WithStack(gcserr.NewProcessDoesNotExistError(pid)) + } + } else { + c.containerCacheMutex.Lock() + defer c.containerCacheMutex.Unlock() + containerEntry := c.getContainer(id) + if containerEntry == nil { + return errors.WithStack(gcserr.NewContainerDoesNotExistError(id)) + } + containerEntry.processCacheMutex.Lock() + defer containerEntry.processCacheMutex.Unlock() + if entry, ok = containerEntry.processCache[pid]; !ok { return errors.WithStack(gcserr.NewProcessDoesNotExistError(pid)) } } @@ -580,6 +599,43 @@ func (c *gcsCore) RegisterProcessExitHook(pid int, exitHook func(oslayer.Process return nil } +func (c *gcsCore) ResizeConsole(id string, pid int, height, width uint16) error { + var ( + p *processCacheEntry + ok bool + ) + + if id == "" { + c.externalProcessCacheMutex.Lock() + if p, ok = c.externalProcessCache[pid]; !ok { + c.externalProcessCacheMutex.Unlock() + return errors.WithStack(gcserr.NewProcessDoesNotExistError(pid)) + } + c.externalProcessCacheMutex.Unlock() + } else { + c.containerCacheMutex.Lock() + containerEntry := c.getContainer(id) + if containerEntry == nil { + c.containerCacheMutex.Unlock() + return errors.WithStack(gcserr.NewContainerDoesNotExistError(id)) + } + containerEntry.processCacheMutex.Lock() + if p, ok = containerEntry.processCache[pid]; !ok { + containerEntry.processCacheMutex.Unlock() + c.containerCacheMutex.Unlock() + return errors.WithStack(gcserr.NewProcessDoesNotExistError(pid)) + } + containerEntry.processCacheMutex.Unlock() + c.containerCacheMutex.Unlock() + } + + if err := stdio.ResizeConsole(p.Master, height, width); err != nil { + return errors.Wrapf(err, "failed to resize console for pid: %d", pid) + } + + return nil +} + // setupMappedVirtualDisks is a helper function which calls into the functions // in storage.go to set up a set of mapped virtual disks for a given container. // It then adds them to the container's cache entry. diff --git a/service/gcs/runtime/runc/runc.go b/service/gcs/runtime/runc/runc.go index 441f5390..2aa6d08e 100644 --- a/service/gcs/runtime/runc/runc.go +++ b/service/gcs/runtime/runc/runc.go @@ -48,16 +48,25 @@ func (c *container) Pid() int { return c.init.Pid() } +func (c *container) Console() *os.File { + return c.init.master +} + type process struct { - c *container - pid int - relay *stdio.TtyRelay + c *container + pid int + relay *stdio.TtyRelay + master *os.File } func (p *process) Pid() int { return p.pid } +func (p *process) Console() *os.File { + return p.master +} + // NewRuntime instantiates a new runcRuntime struct. func NewRuntime() (*runcRuntime, error) { rtime := &runcRuntime{} @@ -528,8 +537,8 @@ func (c *container) startProcess(tempProcessDir string, hasTerminal bool, stdioS } var relay *stdio.TtyRelay + var master *os.File if hasTerminal { - var master *os.File master, err = c.r.getMasterFromSocket(sockListener) if err != nil { cmd.Process.Kill() @@ -561,5 +570,5 @@ func (c *container) startProcess(tempProcessDir string, hasTerminal bool, stdioS if relay != nil { relay.Start() } - return &process{c: c, pid: pid, relay: relay}, nil + return &process{c: c, pid: pid, relay: relay, master: master}, nil } diff --git a/service/gcs/runtime/runtime.go b/service/gcs/runtime/runtime.go index 9c4806a4..0ff7501d 100644 --- a/service/gcs/runtime/runtime.go +++ b/service/gcs/runtime/runtime.go @@ -4,6 +4,7 @@ package runtime import ( "io" + "os" "github.com/Microsoft/opengcs/service/gcs/oslayer" "github.com/Microsoft/opengcs/service/gcs/stdio" @@ -43,6 +44,7 @@ type Process interface { Wait() (oslayer.ProcessExitState, error) Pid() int Delete() error + Console() *os.File } // Container is an interface to manipulate container state. diff --git a/service/gcs/stdio/tty.go b/service/gcs/stdio/tty.go index d2cdafd9..7524de9f 100644 --- a/service/gcs/stdio/tty.go +++ b/service/gcs/stdio/tty.go @@ -7,8 +7,17 @@ import ( "unsafe" "github.com/pkg/errors" + + "golang.org/x/sys/unix" ) +type consoleSize struct { + Height uint16 + Width uint16 + x uint16 + y uint16 +} + // NewConsole allocates a new console and returns the File for its master and // path for its slave. func NewConsole() (*os.File, string, error) { @@ -33,6 +42,15 @@ func NewConsole() (*os.File, string, error) { return master, console, nil } +// ResizeConsole sends the appropriate resize to a pTTY FD +func ResizeConsole(master *os.File, height, width uint16) error { + if master == nil { + return errors.New("failed to resize console with null master fd") + } + + return ioctl(master.Fd(), uintptr(unix.TIOCSWINSZ), uintptr(unsafe.Pointer(&consoleSize{Height: height, Width: width}))) +} + func ioctl(fd uintptr, flag, data uintptr) error { if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, flag, data); err != 0 { return err