Skip to content

Commit

Permalink
feature: add cli attach command
Browse files Browse the repository at this point in the history
Signed-off-by: zhangyue <zy675793960@yeah.net>
  • Loading branch information
zhangyue committed Sep 18, 2018
1 parent 318e7fd commit 4218820
Show file tree
Hide file tree
Showing 7 changed files with 421 additions and 0 deletions.
110 changes: 110 additions & 0 deletions cli/attach.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package main

import (
"context"
"fmt"
"io"
"os"

"github.com/spf13/cobra"

"github.com/alibaba/pouch/pkg/ioutils"
"github.com/alibaba/pouch/pkg/term"
)

// AttachDescription is used to describe attach command in detail and auto generate command doc.
var AttachDescription = "Attach local standard input, output, and error streams to a running container"

// AttachCommand is used to implement 'attach' command.
type AttachCommand struct {
baseCommand

// flags for attach command
noStdin bool
detachKeys string
}

// Init initialize "attach" command.
func (ac *AttachCommand) Init(c *Cli) {
ac.cli = c
ac.cmd = &cobra.Command{
Use: "attach [OPTIONS] CONTAINER",
Short: "Attach local standard input, output, and error streams to a running container",
Long: AttachDescription,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return ac.runAttach(args)
},
Example: ac.example(),
}
ac.addFlags()
}

// addFlags adds flags for specific command.
func (ac *AttachCommand) addFlags() {
flagSet := ac.cmd.Flags()
flagSet.BoolVar(&ac.noStdin, "no-stdin", false, "Do not attach STDIN")
flagSet.StringVar(&ac.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
}

// runAttach is used to attach a container.
func (ac *AttachCommand) runAttach(args []string) error {
name := args[0]

ctx := context.Background()
apiClient := ac.cli.Client()

in, out, err := setRawMode(!ac.noStdin, false)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to set raw mode")
return fmt.Errorf("failed to set raw mode")
}
defer func() {
if err := restoreMode(in, out); err != nil {
fmt.Fprintf(os.Stderr, "failed to restore term mode")
}
}()

conn, br, err := apiClient.ContainerAttach(ctx, name, !ac.noStdin)
if err != nil {
return fmt.Errorf("failed to attach container: %v", err)
}
defer conn.Close()

var inReader io.Reader = os.Stdin
escapeKeys := term.DefaultEscapeKeys
// Wrap the input to detect detach escape sequence.
// Use default escape keys if an invalid sequence is given.
if ac.detachKeys != "" {
customEscapeKeys, err := term.ToBytes(ac.detachKeys)
if err != nil {
return fmt.Errorf("Invalid detach keys (%s) provided", ac.detachKeys)
} else {
escapeKeys = customEscapeKeys
}
}
inReader = ioutils.NewReadCloserWrapper(term.NewEscapeProxy(os.Stdin, escapeKeys), os.Stdin.Close)

wait := make(chan struct{})
go func() {
io.Copy(os.Stdout, br)
wait <- struct{}{}
}()
go func() {
_, err := io.Copy(conn, inReader)
if _, ok := err.(term.EscapeError); ok {
wait <- struct{}{}
}
}()

<-wait
return nil
}

// example shows examples in attach command, and is used in auto-generated cli docs.
func (ac *AttachCommand) example() string {
return `$ pouch ps
Name ID Status Image Runtime
foo 71b9c1 Running docker.io/library/busybox:latest runc
$ pouch attach foo`
}
1 change: 1 addition & 0 deletions cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ func main() {
cli.AddCommand(base, &PullCommand{})
cli.AddCommand(base, &CreateCommand{})
cli.AddCommand(base, &StartCommand{})
cli.AddCommand(base, &AttachCommand{})
cli.AddCommand(base, &StopCommand{})
cli.AddCommand(base, &PsCommand{})
cli.AddCommand(base, &RmCommand{})
Expand Down
25 changes: 25 additions & 0 deletions pkg/ioutils/readers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package ioutils

import "io"

// ReadCloserWrapper wraps an io.Reader, and implements an io.ReadCloser
// It calls the given callback function when closed. It should be constructed
// with NewReadCloserWrapper
type ReadCloserWrapper struct {
io.Reader
closer func() error
}

// NewReadCloserWrapper returns a new io.ReadCloser.
func NewReadCloserWrapper(r io.Reader, closer func() error) io.ReadCloser {
return &ReadCloserWrapper{
Reader: r,
closer: closer,
}

}

// Close calls back the passed closer function
func (r *ReadCloserWrapper) Close() error {
return r.closer()
}
66 changes: 66 additions & 0 deletions pkg/term/ascii.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package term

import (
"fmt"
"strings"
)

// ASCII list the possible supported ASCII key sequence
var ASCII = []string{
"ctrl-@",
"ctrl-a",
"ctrl-b",
"ctrl-c",
"ctrl-d",
"ctrl-e",
"ctrl-f",
"ctrl-g",
"ctrl-h",
"ctrl-i",
"ctrl-j",
"ctrl-k",
"ctrl-l",
"ctrl-m",
"ctrl-n",
"ctrl-o",
"ctrl-p",
"ctrl-q",
"ctrl-r",
"ctrl-s",
"ctrl-t",
"ctrl-u",
"ctrl-v",
"ctrl-w",
"ctrl-x",
"ctrl-y",
"ctrl-z",
"ctrl-[",
"ctrl-\\",
"ctrl-]",
"ctrl-^",
"ctrl-_",
}

// ToBytes converts a string representing a suite of key-sequence to the corresponding ASCII code.
func ToBytes(keys string) ([]byte, error) {
codes := []byte{}
next:
for _, key := range strings.Split(keys, ",") {
if len(key) != 1 {
for code, ctrl := range ASCII {
if ctrl == key {
codes = append(codes, byte(code))
continue next
}
}
if key == "DEL" {
codes = append(codes, 127)
} else {
return nil, fmt.Errorf("Unknown character: '%s'", key)
}
} else {
codes = append(codes, key[0])
}
}
return codes, nil
}
24 changes: 24 additions & 0 deletions pkg/term/ascii_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package term

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestToBytes(t *testing.T) {
codes, err := ToBytes("ctrl-a,a")
assert.NoError(t, err)
assert.Equal(t, []byte{1, 97}, codes)

_, err = ToBytes("shift-z")
assert.Error(t, err)

codes, err = ToBytes("ctrl-@,ctrl-[,~,ctrl-o")
assert.NoError(t, err)
assert.Equal(t, []byte{0, 27, 126, 15}, codes)

codes, err = ToBytes("DEL,+")
assert.NoError(t, err)
assert.Equal(t, []byte{127, 43}, codes)
}
81 changes: 81 additions & 0 deletions pkg/term/proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package term

import (
"io"
)

// DefaultEscapeKeys represents the default escape key sequence: ctrl-p, ctrl-q
var DefaultEscapeKeys = []byte{16, 17}

// EscapeError is special error which returned by a TTY proxy reader's Read()
// method in case its detach escape sequence is read.
type EscapeError struct{}

func (EscapeError) Error() string {
return "read escape sequence"
}

// escapeProxy is used only for attaches with a TTY. It is used to proxy
// stdin keypresses from the underlying reader and look for the passed in
// escape key sequence to signal a detach.
type escapeProxy struct {
escapeKeys []byte
escapeKeyPos int
r io.Reader
}

// NewEscapeProxy returns a new TTY proxy reader which wraps the given reader
// and detects when the specified escape keys are read, in which case the Read
// method will return an error of type EscapeError.
func NewEscapeProxy(r io.Reader, escapeKeys []byte) io.Reader {
return &escapeProxy{
escapeKeys: escapeKeys,
r: r,
}
}

func (r *escapeProxy) Read(buf []byte) (int, error) {
nr, err := r.r.Read(buf)

if len(r.escapeKeys) == 0 {
return nr, err
}

preserve := func() {
// this preserves the original key presses in the passed in buffer
nr += r.escapeKeyPos
preserve := make([]byte, 0, r.escapeKeyPos+len(buf))
preserve = append(preserve, r.escapeKeys[:r.escapeKeyPos]...)
preserve = append(preserve, buf...)
r.escapeKeyPos = 0
copy(buf[0:nr], preserve)
}

if nr != 1 || err != nil {
if r.escapeKeyPos > 0 {
preserve()
}
return nr, err
}

if buf[0] != r.escapeKeys[r.escapeKeyPos] {
if r.escapeKeyPos > 0 {
preserve()
}
return nr, nil
}

if r.escapeKeyPos == len(r.escapeKeys)-1 {
return 0, EscapeError{}
}

// Looks like we've got an escape key, but we need to match again on the next
// read.
// Store the current escape key we found so we can look for the next one on
// the next read.
// Since this is an escape key, make sure we don't let the caller read it
// If later on we find that this is not the escape sequence, we'll add the
// keys back
r.escapeKeyPos++
return nr - r.escapeKeyPos, nil
}
Loading

0 comments on commit 4218820

Please sign in to comment.