Fix Windows terminal handling

Fix some issues with Windows terminal handling with respect to TTYs that came up as part of the
code that adds support for terminal resizing.
pull/6/head
Andy Goldstein 2016-07-15 15:56:42 -04:00
parent 77037722a3
commit 77b0547b3d
10 changed files with 283 additions and 131 deletions

View File

@ -17,15 +17,14 @@ limitations under the License.
package main package main
import ( import (
"github.com/docker/docker/pkg/term" "os"
"k8s.io/kubernetes/pkg/kubectl/cmd" "k8s.io/kubernetes/pkg/kubectl/cmd"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
) )
func NewKubectlServer() *Server { func NewKubectlServer() *Server {
// need to use term.StdStreams to get the right IO refs on Windows cmd := cmd.NewKubectlCommand(cmdutil.NewFactory(nil), os.Stdin, os.Stdout, os.Stderr)
stdin, stdout, stderr := term.StdStreams()
cmd := cmd.NewKubectlCommand(cmdutil.NewFactory(nil), stdin, stdout, stderr)
localFlags := cmd.LocalFlags() localFlags := cmd.LocalFlags()
localFlags.SetInterspersed(false) localFlags.SetInterspersed(false)

View File

@ -17,7 +17,7 @@ limitations under the License.
package app package app
import ( import (
"github.com/docker/docker/pkg/term" "os"
"k8s.io/kubernetes/pkg/kubectl/cmd" "k8s.io/kubernetes/pkg/kubectl/cmd"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
@ -28,8 +28,6 @@ WARNING: this logic is duplicated, with minor changes, in cmd/hyperkube/kubectl.
Any salient changes here will need to be manually reflected in that file. Any salient changes here will need to be manually reflected in that file.
*/ */
func Run() error { func Run() error {
// need to use term.StdStreams to get the right IO refs on Windows cmd := cmd.NewKubectlCommand(cmdutil.NewFactory(nil), os.Stdin, os.Stdout, os.Stderr)
stdin, stdout, stderr := term.StdStreams()
cmd := cmd.NewKubectlCommand(cmdutil.NewFactory(nil), stdin, stdout, stderr)
return cmd.Execute() return cmd.Execute()
} }

View File

@ -32,7 +32,6 @@ import (
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
remotecommandserver "k8s.io/kubernetes/pkg/kubelet/server/remotecommand" remotecommandserver "k8s.io/kubernetes/pkg/kubelet/server/remotecommand"
utilerrors "k8s.io/kubernetes/pkg/util/errors" utilerrors "k8s.io/kubernetes/pkg/util/errors"
"k8s.io/kubernetes/pkg/util/interrupt"
"k8s.io/kubernetes/pkg/util/term" "k8s.io/kubernetes/pkg/util/term"
) )
@ -51,9 +50,11 @@ var (
func NewCmdAttach(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *cobra.Command { func NewCmdAttach(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *cobra.Command {
options := &AttachOptions{ options := &AttachOptions{
StreamOptions: StreamOptions{
In: cmdIn, In: cmdIn,
Out: cmdOut, Out: cmdOut,
Err: cmdErr, Err: cmdErr,
},
Attach: &DefaultRemoteAttach{}, Attach: &DefaultRemoteAttach{},
} }
@ -100,20 +101,10 @@ func (*DefaultRemoteAttach) Attach(method string, url *url.URL, config *restclie
// AttachOptions declare the arguments accepted by the Exec command // AttachOptions declare the arguments accepted by the Exec command
type AttachOptions struct { type AttachOptions struct {
Namespace string StreamOptions
PodName string
ContainerName string
Stdin bool
TTY bool
CommandName string CommandName string
// InterruptParent, if set, is used to handle interrupts while attached
InterruptParent *interrupt.Handler
In io.Reader
Out io.Writer
Err io.Writer
Pod *api.Pod Pod *api.Pod
Attach RemoteAttach Attach RemoteAttach
@ -188,38 +179,30 @@ func (p *AttachOptions) Run() error {
} }
pod := p.Pod pod := p.Pod
// ensure we can recover the terminal while attached
t := term.TTY{
Parent: p.InterruptParent,
Out: p.Out,
}
// check for TTY // check for TTY
tty := p.TTY
containerToAttach, err := p.containerToAttachTo(pod) containerToAttach, err := p.containerToAttachTo(pod)
if err != nil { if err != nil {
return fmt.Errorf("cannot attach to the container: %v", err) return fmt.Errorf("cannot attach to the container: %v", err)
} }
if tty && !containerToAttach.TTY { if p.TTY && !containerToAttach.TTY {
tty = false p.TTY = false
if p.Err != nil {
fmt.Fprintf(p.Err, "Unable to use a TTY - container %s did not allocate one\n", containerToAttach.Name) fmt.Fprintf(p.Err, "Unable to use a TTY - container %s did not allocate one\n", containerToAttach.Name)
} }
if p.Stdin { } else if !p.TTY && containerToAttach.TTY {
t.In = p.In // the container was launched with a TTY, so we have to force a TTY here, otherwise you'll get
if tty && !t.IsTerminalIn() { // an error "Unrecognized input header"
tty = false p.TTY = true
fmt.Fprintln(p.Err, "Unable to use a TTY - input is not a terminal or the right kind of file")
} }
} else {
p.In = nil // ensure we can recover the terminal while attached
} t := p.setupTTY()
t.Raw = tty
// save p.Err so we can print the command prompt message below // save p.Err so we can print the command prompt message below
stderr := p.Err stderr := p.Err
var sizeQueue term.TerminalSizeQueue var sizeQueue term.TerminalSizeQueue
if tty { if t.Raw {
if size := t.GetSize(); size != nil { if size := t.GetSize(); size != nil {
// fake resizing +1 and then back to normal so that attach-detach-reattach will result in the // fake resizing +1 and then back to normal so that attach-detach-reattach will result in the
// screen being redrawn // screen being redrawn
@ -252,17 +235,17 @@ func (p *AttachOptions) Run() error {
Stdin: p.Stdin, Stdin: p.Stdin,
Stdout: p.Out != nil, Stdout: p.Out != nil,
Stderr: p.Err != nil, Stderr: p.Err != nil,
TTY: tty, TTY: t.Raw,
}, api.ParameterCodec) }, api.ParameterCodec)
return p.Attach.Attach("POST", req.URL(), p.Config, p.In, p.Out, p.Err, tty, sizeQueue) return p.Attach.Attach("POST", req.URL(), p.Config, p.In, p.Out, p.Err, t.Raw, sizeQueue)
} }
if err := t.Safe(fn); err != nil { if err := t.Safe(fn); err != nil {
return err return err
} }
if p.Stdin && tty && pod.Spec.RestartPolicy == api.RestartPolicyAlways { if p.Stdin && t.Raw && pod.Spec.RestartPolicy == api.RestartPolicyAlways {
fmt.Fprintf(p.Out, "Session ended, resume using '%s %s -c %s -i -t' command when the pod is running\n", p.CommandName, pod.Name, containerToAttach.Name) fmt.Fprintf(p.Out, "Session ended, resume using '%s %s -c %s -i -t' command when the pod is running\n", p.CommandName, pod.Name, containerToAttach.Name)
} }
return nil return nil

View File

@ -74,21 +74,21 @@ func TestPodAndContainerAttach(t *testing.T) {
name: "no container, no flags", name: "no container, no flags",
}, },
{ {
p: &AttachOptions{ContainerName: "bar"}, p: &AttachOptions{StreamOptions: StreamOptions{ContainerName: "bar"}},
args: []string{"foo"}, args: []string{"foo"},
expectedPod: "foo", expectedPod: "foo",
expectedContainer: "bar", expectedContainer: "bar",
name: "container in flag", name: "container in flag",
}, },
{ {
p: &AttachOptions{ContainerName: "initfoo"}, p: &AttachOptions{StreamOptions: StreamOptions{ContainerName: "initfoo"}},
args: []string{"foo"}, args: []string{"foo"},
expectedPod: "foo", expectedPod: "foo",
expectedContainer: "initfoo", expectedContainer: "initfoo",
name: "init container in flag", name: "init container in flag",
}, },
{ {
p: &AttachOptions{ContainerName: "bar"}, p: &AttachOptions{StreamOptions: StreamOptions{ContainerName: "bar"}},
args: []string{"foo", "-c", "wrong"}, args: []string{"foo", "-c", "wrong"},
expectError: true, expectError: true,
name: "non-existing container in flag", name: "non-existing container in flag",
@ -187,10 +187,12 @@ func TestAttach(t *testing.T) {
remoteAttach.err = fmt.Errorf("attach error") remoteAttach.err = fmt.Errorf("attach error")
} }
params := &AttachOptions{ params := &AttachOptions{
StreamOptions: StreamOptions{
ContainerName: test.container, ContainerName: test.container,
In: bufIn, In: bufIn,
Out: bufOut, Out: bufOut,
Err: bufErr, Err: bufErr,
},
Attach: remoteAttach, Attach: remoteAttach,
} }
cmd := &cobra.Command{} cmd := &cobra.Command{}
@ -261,12 +263,14 @@ func TestAttachWarnings(t *testing.T) {
bufIn := bytes.NewBuffer([]byte{}) bufIn := bytes.NewBuffer([]byte{})
ex := &fakeRemoteAttach{} ex := &fakeRemoteAttach{}
params := &AttachOptions{ params := &AttachOptions{
StreamOptions: StreamOptions{
ContainerName: test.container, ContainerName: test.container,
In: bufIn, In: bufIn,
Out: bufOut, Out: bufOut,
Err: bufErr, Err: bufErr,
Stdin: test.stdin, Stdin: test.stdin,
TTY: test.tty, TTY: test.tty,
},
Attach: ex, Attach: ex,
} }
cmd := &cobra.Command{} cmd := &cobra.Command{}

View File

@ -21,6 +21,7 @@ import (
"io" "io"
"net/url" "net/url"
dockerterm "github.com/docker/docker/pkg/term"
"github.com/golang/glog" "github.com/golang/glog"
"github.com/renstrom/dedent" "github.com/renstrom/dedent"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -49,9 +50,11 @@ var (
func NewCmdExec(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *cobra.Command { func NewCmdExec(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *cobra.Command {
options := &ExecOptions{ options := &ExecOptions{
StreamOptions: StreamOptions{
In: cmdIn, In: cmdIn,
Out: cmdOut, Out: cmdOut,
Err: cmdErr, Err: cmdErr,
},
Executor: &DefaultRemoteExecutor{}, Executor: &DefaultRemoteExecutor{},
} }
@ -98,22 +101,29 @@ func (*DefaultRemoteExecutor) Execute(method string, url *url.URL, config *restc
}) })
} }
// ExecOptions declare the arguments accepted by the Exec command type StreamOptions struct {
type ExecOptions struct {
Namespace string Namespace string
PodName string PodName string
ContainerName string ContainerName string
Stdin bool Stdin bool
TTY bool TTY bool
Command []string
// InterruptParent, if set, is used to handle interrupts while attached // InterruptParent, if set, is used to handle interrupts while attached
InterruptParent *interrupt.Handler InterruptParent *interrupt.Handler
In io.Reader In io.Reader
Out io.Writer Out io.Writer
Err io.Writer Err io.Writer
// for testing
overrideStreams func() (io.ReadCloser, io.Writer, io.Writer)
isTerminalIn func(t term.TTY) bool
}
// ExecOptions declare the arguments accepted by the Exec command
type ExecOptions struct {
StreamOptions
Command []string
Executor RemoteExecutor Executor RemoteExecutor
Client *client.Client Client *client.Client
Config *restclient.Config Config *restclient.Config
@ -177,6 +187,58 @@ func (p *ExecOptions) Validate() error {
return nil return nil
} }
func (o *StreamOptions) setupTTY() term.TTY {
t := term.TTY{
Parent: o.InterruptParent,
Out: o.Out,
}
if !o.Stdin {
// need to nil out o.In to make sure we don't create a stream for stdin
o.In = nil
o.TTY = false
return t
}
t.In = o.In
if !o.TTY {
return t
}
if o.isTerminalIn == nil {
o.isTerminalIn = func(tty term.TTY) bool {
return tty.IsTerminalIn()
}
}
if !o.isTerminalIn(t) {
o.TTY = false
if o.Err != nil {
fmt.Fprintln(o.Err, "Unable to use a TTY - input is not a terminal or the right kind of file")
}
return t
}
// if we get to here, the user wants to attach stdin, wants a TTY, and o.In is a terminal, so we
// can safely set t.Raw to true
t.Raw = true
if o.overrideStreams == nil {
// use dockerterm.StdStreams() to get the right I/O handles on Windows
o.overrideStreams = dockerterm.StdStreams
}
stdin, stdout, _ := o.overrideStreams()
o.In = stdin
t.In = stdin
if o.Out != nil {
o.Out = stdout
t.Out = stdout
}
return t
}
// Run executes a validated remote execution against a pod. // Run executes a validated remote execution against a pod.
func (p *ExecOptions) Run() error { func (p *ExecOptions) Run() error {
pod, err := p.Client.Pods(p.Namespace).Get(p.PodName) pod, err := p.Client.Pods(p.Namespace).Get(p.PodName)
@ -195,26 +257,10 @@ func (p *ExecOptions) Run() error {
} }
// ensure we can recover the terminal while attached // ensure we can recover the terminal while attached
t := term.TTY{ t := p.setupTTY()
Parent: p.InterruptParent,
Out: p.Out,
}
// check for TTY
tty := p.TTY
if p.Stdin {
t.In = p.In
if tty && !t.IsTerminalIn() {
tty = false
fmt.Fprintln(p.Err, "Unable to use a TTY - input is not a terminal or the right kind of file")
}
} else {
p.In = nil
}
t.Raw = tty
var sizeQueue term.TerminalSizeQueue var sizeQueue term.TerminalSizeQueue
if tty { if t.Raw {
// this call spawns a goroutine to monitor/update the terminal size // this call spawns a goroutine to monitor/update the terminal size
sizeQueue = t.MonitorSize(t.GetSize()) sizeQueue = t.MonitorSize(t.GetSize())
@ -237,10 +283,10 @@ func (p *ExecOptions) Run() error {
Stdin: p.Stdin, Stdin: p.Stdin,
Stdout: p.Out != nil, Stdout: p.Out != nil,
Stderr: p.Err != nil, Stderr: p.Err != nil,
TTY: tty, TTY: t.Raw,
}, api.ParameterCodec) }, api.ParameterCodec)
return p.Executor.Execute("POST", req.URL(), p.Config, p.In, p.Out, p.Err, tty, sizeQueue) return p.Executor.Execute("POST", req.URL(), p.Config, p.In, p.Out, p.Err, t.Raw, sizeQueue)
} }
if err := t.Safe(fn); err != nil { if err := t.Safe(fn); err != nil {

View File

@ -20,9 +20,11 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"reflect" "reflect"
"strings"
"testing" "testing"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -65,19 +67,19 @@ func TestPodAndContainer(t *testing.T) {
name: "empty", name: "empty",
}, },
{ {
p: &ExecOptions{PodName: "foo"}, p: &ExecOptions{StreamOptions: StreamOptions{PodName: "foo"}},
argsLenAtDash: -1, argsLenAtDash: -1,
expectError: true, expectError: true,
name: "no cmd", name: "no cmd",
}, },
{ {
p: &ExecOptions{PodName: "foo", ContainerName: "bar"}, p: &ExecOptions{StreamOptions: StreamOptions{PodName: "foo", ContainerName: "bar"}},
argsLenAtDash: -1, argsLenAtDash: -1,
expectError: true, expectError: true,
name: "no cmd, w/ container", name: "no cmd, w/ container",
}, },
{ {
p: &ExecOptions{PodName: "foo"}, p: &ExecOptions{StreamOptions: StreamOptions{PodName: "foo"}},
args: []string{"cmd"}, args: []string{"cmd"},
argsLenAtDash: -1, argsLenAtDash: -1,
expectedPod: "foo", expectedPod: "foo",
@ -115,7 +117,7 @@ func TestPodAndContainer(t *testing.T) {
name: "cmd, cmd is behind dash", name: "cmd, cmd is behind dash",
}, },
{ {
p: &ExecOptions{ContainerName: "bar"}, p: &ExecOptions{StreamOptions: StreamOptions{ContainerName: "bar"}},
args: []string{"foo", "cmd"}, args: []string{"foo", "cmd"},
argsLenAtDash: -1, argsLenAtDash: -1,
expectedPod: "foo", expectedPod: "foo",
@ -206,11 +208,13 @@ func TestExec(t *testing.T) {
ex.execErr = fmt.Errorf("exec error") ex.execErr = fmt.Errorf("exec error")
} }
params := &ExecOptions{ params := &ExecOptions{
StreamOptions: StreamOptions{
PodName: "foo", PodName: "foo",
ContainerName: "bar", ContainerName: "bar",
In: bufIn, In: bufIn,
Out: bufOut, Out: bufOut,
Err: bufErr, Err: bufErr,
},
Executor: ex, Executor: ex,
} }
cmd := &cobra.Command{} cmd := &cobra.Command{}
@ -257,3 +261,124 @@ func execPod() *api.Pod {
}, },
} }
} }
func TestSetupTTY(t *testing.T) {
stderr := &bytes.Buffer{}
// test 1 - don't attach stdin
o := &StreamOptions{
// InterruptParent: ,
Stdin: false,
In: &bytes.Buffer{},
Out: &bytes.Buffer{},
Err: stderr,
TTY: true,
}
tty := o.setupTTY()
if o.In != nil {
t.Errorf("don't attach stdin: o.In should be nil")
}
if tty.In != nil {
t.Errorf("don't attach stdin: tty.In should be nil")
}
if o.TTY {
t.Errorf("don't attach stdin: o.TTY should be false")
}
if tty.Raw {
t.Errorf("don't attach stdin: tty.Raw should be false")
}
if len(stderr.String()) > 0 {
t.Errorf("don't attach stdin: stderr wasn't empty: %s", stderr.String())
}
// tests from here on attach stdin
// test 2 - don't request a TTY
o.Stdin = true
o.In = &bytes.Buffer{}
o.TTY = false
tty = o.setupTTY()
if o.In == nil {
t.Errorf("attach stdin, no TTY: o.In should not be nil")
}
if tty.In != o.In {
t.Errorf("attach stdin, no TTY: tty.In should equal o.In")
}
if o.TTY {
t.Errorf("attach stdin, no TTY: o.TTY should be false")
}
if tty.Raw {
t.Errorf("attach stdin, no TTY: tty.Raw should be false")
}
if len(stderr.String()) > 0 {
t.Errorf("attach stdin, no TTY: stderr wasn't empty: %s", stderr.String())
}
// test 3 - request a TTY, but stdin is not a terminal
o.Stdin = true
o.In = &bytes.Buffer{}
o.Err = stderr
o.TTY = true
tty = o.setupTTY()
if o.In == nil {
t.Errorf("attach stdin, TTY, not a terminal: o.In should not be nil")
}
if tty.In != o.In {
t.Errorf("attach stdin, TTY, not a terminal: tty.In should equal o.In")
}
if o.TTY {
t.Errorf("attach stdin, TTY, not a terminal: o.TTY should be false")
}
if tty.Raw {
t.Errorf("attach stdin, TTY, not a terminal: tty.Raw should be false")
}
if !strings.Contains(stderr.String(), "input is not a terminal") {
t.Errorf("attach stdin, TTY, not a terminal: expected 'input is not a terminal' to stderr")
}
// test 4 - request a TTY, stdin is a terminal
o.Stdin = true
o.In = &bytes.Buffer{}
stderr.Reset()
o.TTY = true
overrideStdin := ioutil.NopCloser(&bytes.Buffer{})
overrideStdout := &bytes.Buffer{}
overrideStderr := &bytes.Buffer{}
o.overrideStreams = func() (io.ReadCloser, io.Writer, io.Writer) {
return overrideStdin, overrideStdout, overrideStderr
}
o.isTerminalIn = func(tty term.TTY) bool {
return true
}
tty = o.setupTTY()
if o.In != overrideStdin {
t.Errorf("attach stdin, TTY, is a terminal: o.In should equal overrideStdin")
}
if tty.In != o.In {
t.Errorf("attach stdin, TTY, is a terminal: tty.In should equal o.In")
}
if !o.TTY {
t.Errorf("attach stdin, TTY, is a terminal: o.TTY should be true")
}
if !tty.Raw {
t.Errorf("attach stdin, TTY, is a terminal: tty.Raw should be true")
}
if len(stderr.String()) > 0 {
t.Errorf("attach stdin, TTY, is a terminal: stderr wasn't empty: %s", stderr.String())
}
if o.Out != overrideStdout {
t.Errorf("attach stdin, TTY, is a terminal: o.Out should equal overrideStdout")
}
if tty.Out != o.Out {
t.Errorf("attach stdin, TTY, is a terminal: tty.Out should equal o.Out")
}
}

View File

@ -226,11 +226,13 @@ func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cob
if attach { if attach {
opts := &AttachOptions{ opts := &AttachOptions{
StreamOptions: StreamOptions{
In: cmdIn, In: cmdIn,
Out: cmdOut, Out: cmdOut,
Err: cmdErr, Err: cmdErr,
Stdin: interactive, Stdin: interactive,
TTY: tty, TTY: tty,
},
CommandName: cmd.Parent().CommandPath() + " attach", CommandName: cmd.Parent().CommandPath() + " attach",

View File

@ -32,10 +32,11 @@ type Size struct {
// GetSize returns the current size of the user's terminal. If it isn't a terminal, // GetSize returns the current size of the user's terminal. If it isn't a terminal,
// nil is returned. // nil is returned.
func (t TTY) GetSize() *Size { func (t TTY) GetSize() *Size {
if !t.IsTerminalOut() { outFd, isTerminal := term.GetFdInfo(t.Out)
if !isTerminal {
return nil return nil
} }
return GetSize(t.Out.(fd).Fd()) return GetSize(outFd)
} }
// GetSize returns the current size of the terminal associated with fd. // GetSize returns the current size of the terminal associated with fd.
@ -57,7 +58,8 @@ func SetSize(fd uintptr, size Size) error {
// MonitorSize monitors the terminal's size. It returns a TerminalSizeQueue primed with // MonitorSize monitors the terminal's size. It returns a TerminalSizeQueue primed with
// initialSizes, or nil if there's no TTY present. // initialSizes, or nil if there's no TTY present.
func (t *TTY) MonitorSize(initialSizes ...*Size) TerminalSizeQueue { func (t *TTY) MonitorSize(initialSizes ...*Size) TerminalSizeQueue {
if !t.IsTerminalOut() { outFd, isTerminal := term.GetFdInfo(t.Out)
if !isTerminal {
return nil return nil
} }
@ -69,7 +71,7 @@ func (t *TTY) MonitorSize(initialSizes ...*Size) TerminalSizeQueue {
stopResizing: make(chan struct{}), stopResizing: make(chan struct{}),
} }
t.sizeQueue.monitorSize(initialSizes...) t.sizeQueue.monitorSize(outFd, initialSizes...)
return t.sizeQueue return t.sizeQueue
} }
@ -94,7 +96,7 @@ var _ TerminalSizeQueue = &sizeQueue{}
// monitorSize primes resizeChan with initialSizes and then monitors for resize events. With each // monitorSize primes resizeChan with initialSizes and then monitors for resize events. With each
// new event, it sends the current terminal size to resizeChan. // new event, it sends the current terminal size to resizeChan.
func (s *sizeQueue) monitorSize(initialSizes ...*Size) { func (s *sizeQueue) monitorSize(outFd uintptr, initialSizes ...*Size) {
// send the initial sizes // send the initial sizes
for i := range initialSizes { for i := range initialSizes {
if initialSizes[i] != nil { if initialSizes[i] != nil {
@ -104,7 +106,7 @@ func (s *sizeQueue) monitorSize(initialSizes ...*Size) {
resizeEvents := make(chan Size, 1) resizeEvents := make(chan Size, 1)
monitorResizeEvents(s.t.Out.(fd).Fd(), resizeEvents, s.stopResizing) monitorResizeEvents(outFd, resizeEvents, s.stopResizing)
// listen for resize events in the background // listen for resize events in the background
go func() { go func() {

View File

@ -29,7 +29,11 @@ func monitorResizeEvents(fd uintptr, resizeEvents chan<- Size, stop chan struct{
go func() { go func() {
defer runtime.HandleCrash() defer runtime.HandleCrash()
var lastSize Size size := GetSize(fd)
if size == nil {
return
}
lastSize := *size
for { for {
// see if we need to stop running // see if we need to stop running

View File

@ -52,11 +52,6 @@ type TTY struct {
sizeQueue *sizeQueue sizeQueue *sizeQueue
} }
// fd returns a file descriptor for a given object.
type fd interface {
Fd() uintptr
}
// IsTerminalIn returns true if t.In is a terminal. Does not check /dev/tty // IsTerminalIn returns true if t.In is a terminal. Does not check /dev/tty
// even if TryDev is set. // even if TryDev is set.
func (t TTY) IsTerminalIn() bool { func (t TTY) IsTerminalIn() bool {
@ -71,8 +66,8 @@ func (t TTY) IsTerminalOut() bool {
// IsTerminal returns whether the passed object is a terminal or not // IsTerminal returns whether the passed object is a terminal or not
func IsTerminal(i interface{}) bool { func IsTerminal(i interface{}) bool {
file, ok := i.(fd) _, terminal := term.GetFdInfo(i)
return ok && term.IsTerminal(file.Fd()) return terminal
} }
// Safe invokes the provided function and will attempt to ensure that when the // Safe invokes the provided function and will attempt to ensure that when the
@ -82,22 +77,16 @@ func IsTerminal(i interface{}) bool {
// If the input file descriptor is not a TTY and TryDev is true, the /dev/tty file // If the input file descriptor is not a TTY and TryDev is true, the /dev/tty file
// will be opened (if available). // will be opened (if available).
func (t TTY) Safe(fn SafeFunc) error { func (t TTY) Safe(fn SafeFunc) error {
in := t.In inFd, isTerminal := term.GetFdInfo(t.In)
var hasFd bool if !isTerminal && t.TryDev {
var inFd uintptr
if desc, ok := in.(fd); ok && in != nil {
inFd = desc.Fd()
hasFd = true
}
if t.TryDev && (!hasFd || !term.IsTerminal(inFd)) {
if f, err := os.Open("/dev/tty"); err == nil { if f, err := os.Open("/dev/tty"); err == nil {
defer f.Close() defer f.Close()
inFd = f.Fd() inFd = f.Fd()
hasFd = true isTerminal = term.IsTerminal(inFd)
} }
} }
if !hasFd || !term.IsTerminal(inFd) { if !isTerminal {
return fn() return fn()
} }