diff --git a/README.md b/README.md index dbcad36..dc62bfe 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ Option | Description -s | select initial container sort field -scale-cpu | show cpu as % of system total -v | output version information and exit +-shell | specify shell (default: sh) ### Keybindings @@ -84,6 +85,7 @@ s | Select container sort field r | Reverse container sort order o | Open single view l | View container logs (`t` to toggle timestamp when open) +e | Exec Shell S | Save current configuration to file q | Quit ctop diff --git a/config/param.go b/config/param.go index 30dd036..f0b7ef1 100644 --- a/config/param.go +++ b/config/param.go @@ -12,6 +12,11 @@ var params = []*Param{ Val: "state", Label: "Container Sort Field", }, + &Param{ + Key: "shell", + Val: "sh", + Label: "Shell", + }, } type Param struct { diff --git a/connector/manager/docker.go b/connector/manager/docker.go index 77dc987..1e8a91b 100644 --- a/connector/manager/docker.go +++ b/connector/manager/docker.go @@ -3,6 +3,9 @@ package manager import ( "fmt" api "github.com/fsouza/go-dockerclient" + "github.com/pkg/errors" + "io" + "os" ) type Docker struct { @@ -17,6 +20,85 @@ func NewDocker(client *api.Client, id string) *Docker { } } +// Do not allow to close reader (i.e. /dev/stdin which docker client tries to close after command execution) +type noClosableReader struct { + wrappedReader io.Reader +} + +func (w *noClosableReader) Read(p []byte) (n int, err error) { + return w.wrappedReader.Read(p) +} + +const ( + STDIN = 0 + STDOUT = 1 + STDERR = 2 +) + +var ( + wrongFrameFormat = errors.New("Wrong frame format") +) + +// A frame has a Header and a Payload +// Header: [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4} +// STREAM_TYPE can be: +// 0: stdin (is written on stdout) +// 1: stdout +// 2: stderr +// SIZE1, SIZE2, SIZE3, SIZE4 are the four bytes of the uint32 size encoded as big endian. +// But we don't use size, because we don't need to find the end of frame. +type frameWriter struct { + stdout io.Writer + stderr io.Writer + stdin io.Writer +} + +func (w *frameWriter) Write(p []byte) (n int, err error) { + if len(p) > 8 { + var targetWriter io.Writer + switch p[0] { + case STDIN: + targetWriter = w.stdin + break + case STDOUT: + targetWriter = w.stdout + break + case STDERR: + targetWriter = w.stderr + break + default: + return 0, wrongFrameFormat + } + + n, err := targetWriter.Write(p[8:]) + return n + 8, err + } + + return 0, wrongFrameFormat +} + +func (dc *Docker) Exec(cmd []string) error { + execCmd, err := dc.client.CreateExec(api.CreateExecOptions{ + AttachStdin: true, + AttachStdout: true, + AttachStderr: true, + Cmd: cmd, + Container: dc.id, + Tty: true, + }) + + if err != nil { + return err + } + + return dc.client.StartExec(execCmd.ID, api.StartExecOptions{ + InputStream: &noClosableReader{os.Stdin}, + OutputStream: &frameWriter{os.Stdout, os.Stderr, os.Stdin}, + ErrorStream: os.Stderr, + RawTerminal: true, + }) +} + func (dc *Docker) Start() error { c, err := dc.client.InspectContainer(dc.id) if err != nil { diff --git a/connector/manager/main.go b/connector/manager/main.go index b6debaa..f65aad3 100644 --- a/connector/manager/main.go +++ b/connector/manager/main.go @@ -7,4 +7,5 @@ type Manager interface { Pause() error Unpause() error Restart() error + Exec(cmd []string) error } diff --git a/connector/manager/mock.go b/connector/manager/mock.go index f33fd77..f6fd62f 100644 --- a/connector/manager/mock.go +++ b/connector/manager/mock.go @@ -29,3 +29,7 @@ func (m *Mock) Unpause() error { func (m *Mock) Restart() error { return nil } + +func (m *Mock) Exec(cmd []string) error { + return nil +} diff --git a/connector/manager/runc.go b/connector/manager/runc.go index cf61f14..07a4b58 100644 --- a/connector/manager/runc.go +++ b/connector/manager/runc.go @@ -29,3 +29,7 @@ func (rc *Runc) Unpause() error { func (rc *Runc) Restart() error { return nil } + +func (rc *Runc) Exec(cmd []string) error { + return nil +} diff --git a/container/main.go b/container/main.go index b4bd0b3..932f764 100644 --- a/container/main.go +++ b/container/main.go @@ -153,3 +153,7 @@ func (c *Container) Restart() { } } } + +func (c *Container) Exec(cmd []string) error { + return c.manager.Exec(cmd) +} diff --git a/grid.go b/grid.go index 501c216..871d3cb 100644 --- a/grid.go +++ b/grid.go @@ -116,6 +116,10 @@ func Display() bool { menu = LogMenu ui.StopLoop() }) + ui.Handle("/sys/kbd/e", func(ui.Event) { + menu = ExecShell + ui.StopLoop() + }) ui.Handle("/sys/kbd/o", func(ui.Event) { menu = SingleView ui.StopLoop() diff --git a/main.go b/main.go index 3a93a4c..f2eac66 100644 --- a/main.go +++ b/main.go @@ -45,6 +45,7 @@ func main() { invertFlag = flag.Bool("i", false, "invert default colors") scaleCpu = flag.Bool("scale-cpu", false, "show cpu as % of system total") connectorFlag = flag.String("connector", "docker", "container connector to use") + defaultShell = flag.String("shell", "", "default shell") ) flag.Parse() @@ -87,6 +88,10 @@ func main() { config.Toggle("scaleCpu") } + if *defaultShell != "" { + config.Update("shell", *defaultShell) + } + // init ui if *invertFlag { InvertColorMap() diff --git a/menus.go b/menus.go index 496c726..52d5caf 100644 --- a/menus.go +++ b/menus.go @@ -25,6 +25,7 @@ var helpDialog = []menu.Item{ {"[r] - reverse container sort order", ""}, {"[o] - open single view", ""}, {"[l] - view container logs ([t] to toggle timestamp when open)", ""}, + {"[e] - exec shell", ""}, {"[S] - save current configuration to file", ""}, {"[q] - exit ctop", ""}, } @@ -134,6 +135,7 @@ func ContainerMenu() MenuFn { items = append(items, menu.Item{Val: "stop", Label: "[s] stop"}) items = append(items, menu.Item{Val: "pause", Label: "[p] pause"}) items = append(items, menu.Item{Val: "restart", Label: "[r] restart"}) + items = append(items, menu.Item{Val: "exec shell", Label: "[e]xec shell"}) } if c.Meta["state"] == "exited" || c.Meta["state"] == "created" { items = append(items, menu.Item{Val: "start", Label: "[s] start"}) @@ -210,6 +212,8 @@ func ContainerMenu() MenuFn { nextMenu = SingleView case "logs": nextMenu = LogMenu + case "exec shell": + nextMenu = ExecShell case "start": nextMenu = Confirm(confirmTxt("start", c.GetMeta("name")), c.Start) case "stop": @@ -256,6 +260,24 @@ func LogMenu() MenuFn { return nil } +func ExecShell() MenuFn { + c := cursor.Selected() + + if c == nil { + return nil + } + + ui.DefaultEvtStream.ResetHandlers() + defer ui.DefaultEvtStream.ResetHandlers() + + shell := config.Get("shell") + if err := c.Exec([]string{shell.Val, "-c", "echo '\033[0m' && clear && " + shell.Val}); err != nil { + log.Fatal(err) + } + + return nil +} + // Create a confirmation dialog with a given description string and // func to perform if confirmed func Confirm(txt string, fn func()) MenuFn {