mirror of https://github.com/k3s-io/k3s
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
parent
77037722a3
commit
77b0547b3d
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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{}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue