From f27de1c29e571cbbb9b8b8770cacbbc9e1ab9f6b Mon Sep 17 00:00:00 2001 From: Stanislav Pavlovichev Date: Sun, 7 Oct 2018 16:46:32 +0300 Subject: [PATCH 1/9] Exec Sh Feature --- menus.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/menus.go b/menus.go index 0c819c7..dbd5f70 100644 --- a/menus.go +++ b/menus.go @@ -2,6 +2,8 @@ package main import ( "fmt" + "os" + "os/exec" "time" "github.com/bcicen/ctop/config" @@ -134,6 +136,7 @@ func ContainerMenu() MenuFn { items = append(items, menu.Item{Val: "stop", Label: "stop"}) items = append(items, menu.Item{Val: "pause", Label: "pause"}) items = append(items, menu.Item{Val: "restart", Label: "restart"}) + items = append(items, menu.Item{Val: "exec sh", Label: "exec sh"}) } if c.Meta["state"] == "exited" || c.Meta["state"] == "created" { items = append(items, menu.Item{Val: "start", Label: "start"}) @@ -156,6 +159,8 @@ func ContainerMenu() MenuFn { nextMenu = SingleView case "logs": nextMenu = LogMenu + case "exec sh": + nextMenu = ExecSh case "start": nextMenu = Confirm(confirmTxt("start", c.GetMeta("name")), c.Start) case "stop": @@ -207,6 +212,24 @@ func LogMenu() MenuFn { return nil } +func ExecSh() MenuFn { + c := cursor.Selected() + + if c == nil { + return nil + } + + // Reset colors && clear screen && run sh + cmdName := fmt.Sprintf("echo '\033[0m' && clear && docker exec -it %s sh", c.GetMeta("name")) + cmd := exec.Command("bash", "-c", cmdName) + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + cmd.Run() + + return nil +} + // Create a confirmation dialog with a given description string and // func to perform if confirmed func Confirm(txt string, fn func()) MenuFn { From e68f7ba96a37b174e83cfae2704a54c1515cebc9 Mon Sep 17 00:00:00 2001 From: Stanislav Pavlovichev Date: Fri, 12 Oct 2018 10:03:27 +0300 Subject: [PATCH 2/9] fix: handlers used to work after "exec sh" command feature: hot key for "exec sh" --- grid.go | 4 ++++ menus.go | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/grid.go b/grid.go index 501c216..c646c16 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 = ExecSh + ui.StopLoop() + }) ui.Handle("/sys/kbd/o", func(ui.Event) { menu = SingleView ui.StopLoop() diff --git a/menus.go b/menus.go index dbd5f70..aa445c3 100644 --- a/menus.go +++ b/menus.go @@ -27,6 +27,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 sh", ""}, {"[S] - save current configuration to file", ""}, {"[q] - exit ctop", ""}, } @@ -219,6 +220,11 @@ func ExecSh() MenuFn { return nil } + ui.DefaultEvtStream.ResetHandlers() + defer ui.DefaultEvtStream.ResetHandlers() + ui.StopLoop() + defer ui.Loop() + // Reset colors && clear screen && run sh cmdName := fmt.Sprintf("echo '\033[0m' && clear && docker exec -it %s sh", c.GetMeta("name")) cmd := exec.Command("bash", "-c", cmdName) From 967a87a65f16c08994cc10944cc0d87e425df921 Mon Sep 17 00:00:00 2001 From: Stanislav Pavlovichev Date: Sat, 13 Oct 2018 08:33:53 +0300 Subject: [PATCH 3/9] Exec using API --- Gopkg.lock | 76 +++++++++++++++++++++++++++++++++---- connector/manager/docker.go | 23 +++++++++++ connector/manager/main.go | 1 + connector/manager/mock.go | 4 ++ connector/manager/runc.go | 4 ++ container/main.go | 4 ++ menus.go | 10 +---- 7 files changed, 105 insertions(+), 17 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 8d7a4ce..515b296 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -3,7 +3,10 @@ [[projects]] name = "github.com/Azure/go-ansiterm" - packages = [".","winterm"] + packages = [ + ".", + "winterm" + ] revision = "fa152c58bc15761d0200cb75fe958b89a9d4888e" [[projects]] @@ -36,13 +39,44 @@ [[projects]] name = "github.com/coreos/go-systemd" - packages = ["dbus","util"] + packages = [ + "dbus", + "util" + ] revision = "b4a58d95188dd092ae20072bac14cece0e67c388" version = "v4" [[projects]] name = "github.com/docker/docker" - packages = ["api/types","api/types/blkiodev","api/types/container","api/types/filters","api/types/mount","api/types/network","api/types/registry","api/types/strslice","api/types/swarm","api/types/versions","opts","pkg/archive","pkg/fileutils","pkg/homedir","pkg/idtools","pkg/ioutils","pkg/jsonlog","pkg/jsonmessage","pkg/longpath","pkg/mount","pkg/pools","pkg/promise","pkg/stdcopy","pkg/symlink","pkg/system","pkg/term","pkg/term/windows"] + packages = [ + "api/types", + "api/types/blkiodev", + "api/types/container", + "api/types/filters", + "api/types/mount", + "api/types/network", + "api/types/registry", + "api/types/strslice", + "api/types/swarm", + "api/types/versions", + "opts", + "pkg/archive", + "pkg/fileutils", + "pkg/homedir", + "pkg/idtools", + "pkg/ioutils", + "pkg/jsonlog", + "pkg/jsonmessage", + "pkg/longpath", + "pkg/mount", + "pkg/pools", + "pkg/promise", + "pkg/stdcopy", + "pkg/symlink", + "pkg/system", + "pkg/term", + "pkg/term/windows" + ] revision = "90d35abf7b3535c1c319c872900fbd76374e521c" version = "v17.05.0-ce-rc3" @@ -128,7 +162,24 @@ [[projects]] name = "github.com/opencontainers/runc" - packages = ["libcontainer","libcontainer/apparmor","libcontainer/cgroups","libcontainer/cgroups/fs","libcontainer/cgroups/systemd","libcontainer/configs","libcontainer/configs/validate","libcontainer/criurpc","libcontainer/keys","libcontainer/label","libcontainer/seccomp","libcontainer/selinux","libcontainer/stacktrace","libcontainer/system","libcontainer/user","libcontainer/utils"] + packages = [ + "libcontainer", + "libcontainer/apparmor", + "libcontainer/cgroups", + "libcontainer/cgroups/fs", + "libcontainer/cgroups/systemd", + "libcontainer/configs", + "libcontainer/configs/validate", + "libcontainer/criurpc", + "libcontainer/keys", + "libcontainer/label", + "libcontainer/seccomp", + "libcontainer/selinux", + "libcontainer/stacktrace", + "libcontainer/system", + "libcontainer/user", + "libcontainer/utils" + ] revision = "baf6536d6259209c3edfa2b22237af82942d3dfa" version = "v0.1.1" @@ -144,22 +195,31 @@ [[projects]] name = "github.com/vishvananda/netlink" - packages = [".","nl"] + packages = [ + ".", + "nl" + ] revision = "1e2e08e8a2dcdacaae3f14ac44c5cfa31361f270" [[projects]] name = "golang.org/x/net" - packages = ["context","context/ctxhttp"] + packages = [ + "context", + "context/ctxhttp" + ] revision = "a6577fac2d73be281a500b310739095313165611" [[projects]] name = "golang.org/x/sys" - packages = ["unix","windows"] + packages = [ + "unix", + "windows" + ] revision = "99f16d856c9836c42d24e7ab64ea72916925fa97" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "a21d4c707f08f26de894adbdd00d5d6e82a54f5e0f52566229dd2da9b94e26ec" + inputs-digest = "f46f5c696ecb0b0c42a38dac512df21fc1f5fb2bfda888434e005e69d1b6273b" solver-name = "gps-cdcl" solver-version = 1 diff --git a/connector/manager/docker.go b/connector/manager/docker.go index 77dc987..0f66298 100644 --- a/connector/manager/docker.go +++ b/connector/manager/docker.go @@ -3,6 +3,7 @@ package manager import ( "fmt" api "github.com/fsouza/go-dockerclient" + "os" ) type Docker struct { @@ -17,6 +18,28 @@ func NewDocker(client *api.Client, id string) *Docker { } } +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: os.Stdin, + OutputStream: os.Stdout, + 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 586dd94..735fff1 100644 --- a/container/main.go +++ b/container/main.go @@ -149,3 +149,7 @@ func (c *Container) Restart() { } } } + +func (c *Container) Exec(cmd []string) error { + return c.manager.Exec(cmd) +} diff --git a/menus.go b/menus.go index aa445c3..9ed7542 100644 --- a/menus.go +++ b/menus.go @@ -2,8 +2,6 @@ package main import ( "fmt" - "os" - "os/exec" "time" "github.com/bcicen/ctop/config" @@ -225,13 +223,7 @@ func ExecSh() MenuFn { ui.StopLoop() defer ui.Loop() - // Reset colors && clear screen && run sh - cmdName := fmt.Sprintf("echo '\033[0m' && clear && docker exec -it %s sh", c.GetMeta("name")) - cmd := exec.Command("bash", "-c", cmdName) - cmd.Stdout = os.Stdout - cmd.Stdin = os.Stdin - cmd.Stderr = os.Stderr - cmd.Run() + c.Exec([]string{"sh", "-c", "echo '\033[0m' && clear && sh"}) return nil } From a26fc9169c026a0a3464959c324bd86bf92f687b Mon Sep 17 00:00:00 2001 From: Stanislav Pavlovichev Date: Thu, 25 Oct 2018 21:52:59 +0300 Subject: [PATCH 4/9] Ability to change Shell --- README.md | 2 ++ config/param.go | 5 +++++ grid.go | 2 +- main.go | 5 +++++ menus.go | 15 +++++++++------ 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 99811cc..da732c5 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/grid.go b/grid.go index c646c16..871d3cb 100644 --- a/grid.go +++ b/grid.go @@ -117,7 +117,7 @@ func Display() bool { ui.StopLoop() }) ui.Handle("/sys/kbd/e", func(ui.Event) { - menu = ExecSh + menu = ExecShell ui.StopLoop() }) ui.Handle("/sys/kbd/o", func(ui.Event) { 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 9ed7542..c4f3ac8 100644 --- a/menus.go +++ b/menus.go @@ -25,7 +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 sh", ""}, + {"[e] - exec shell", ""}, {"[S] - save current configuration to file", ""}, {"[q] - exit ctop", ""}, } @@ -135,7 +135,7 @@ func ContainerMenu() MenuFn { items = append(items, menu.Item{Val: "stop", Label: "stop"}) items = append(items, menu.Item{Val: "pause", Label: "pause"}) items = append(items, menu.Item{Val: "restart", Label: "restart"}) - items = append(items, menu.Item{Val: "exec sh", Label: "exec sh"}) + items = append(items, menu.Item{Val: "exec shell", Label: "exec shell"}) } if c.Meta["state"] == "exited" || c.Meta["state"] == "created" { items = append(items, menu.Item{Val: "start", Label: "start"}) @@ -158,8 +158,8 @@ func ContainerMenu() MenuFn { nextMenu = SingleView case "logs": nextMenu = LogMenu - case "exec sh": - nextMenu = ExecSh + case "exec shell": + nextMenu = ExecShell case "start": nextMenu = Confirm(confirmTxt("start", c.GetMeta("name")), c.Start) case "stop": @@ -211,7 +211,7 @@ func LogMenu() MenuFn { return nil } -func ExecSh() MenuFn { +func ExecShell() MenuFn { c := cursor.Selected() if c == nil { @@ -223,7 +223,10 @@ func ExecSh() MenuFn { ui.StopLoop() defer ui.Loop() - c.Exec([]string{"sh", "-c", "echo '\033[0m' && clear && sh"}) + 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 } From d59c91a461827b853b09c0581c1dd5be0f69859f Mon Sep 17 00:00:00 2001 From: Stanislav Pavlovichev Date: Fri, 26 Oct 2018 17:08:33 +0300 Subject: [PATCH 5/9] Do not allow to close /dev/stdin --- connector/manager/docker.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/connector/manager/docker.go b/connector/manager/docker.go index 0f66298..7e32cc7 100644 --- a/connector/manager/docker.go +++ b/connector/manager/docker.go @@ -3,6 +3,7 @@ package manager import ( "fmt" api "github.com/fsouza/go-dockerclient" + "io" "os" ) @@ -18,6 +19,15 @@ 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) +} + func (dc *Docker) Exec(cmd []string) error { execCmd, err := dc.client.CreateExec(api.CreateExecOptions{ AttachStdin: true, @@ -33,7 +43,7 @@ func (dc *Docker) Exec(cmd []string) error { } return dc.client.StartExec(execCmd.ID, api.StartExecOptions{ - InputStream: os.Stdin, + InputStream: &noClosableReader{os.Stdin}, OutputStream: os.Stdout, ErrorStream: os.Stderr, RawTerminal: true, From ca35ef2aab13b5e8bc3de5e8adbd87f17ca98d88 Mon Sep 17 00:00:00 2001 From: Stanislav Pavlovichev Date: Sun, 28 Oct 2018 12:07:43 +0200 Subject: [PATCH 6/9] Unnecessary loop stopping --- menus.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/menus.go b/menus.go index c4f3ac8..5084a46 100644 --- a/menus.go +++ b/menus.go @@ -220,8 +220,6 @@ func ExecShell() MenuFn { ui.DefaultEvtStream.ResetHandlers() defer ui.DefaultEvtStream.ResetHandlers() - ui.StopLoop() - defer ui.Loop() shell := config.Get("shell") if err := c.Exec([]string{shell.Val, "-c", "echo '\033[0m' && clear && " + shell.Val}); err != nil { From 101ddad6926f853eaa8650337bc8cf98fbc6ddf7 Mon Sep 17 00:00:00 2001 From: Stanislav Pavlovichev Date: Sun, 28 Oct 2018 13:42:15 +0200 Subject: [PATCH 7/9] Fixed a problem with rendering --- connector/manager/docker.go | 51 ++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/connector/manager/docker.go b/connector/manager/docker.go index 7e32cc7..1e8a91b 100644 --- a/connector/manager/docker.go +++ b/connector/manager/docker.go @@ -3,6 +3,7 @@ package manager import ( "fmt" api "github.com/fsouza/go-dockerclient" + "github.com/pkg/errors" "io" "os" ) @@ -28,6 +29,54 @@ 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, @@ -44,7 +93,7 @@ func (dc *Docker) Exec(cmd []string) error { return dc.client.StartExec(execCmd.ID, api.StartExecOptions{ InputStream: &noClosableReader{os.Stdin}, - OutputStream: os.Stdout, + OutputStream: &frameWriter{os.Stdout, os.Stderr, os.Stdin}, ErrorStream: os.Stderr, RawTerminal: true, }) From b8c38d09efd54cf27547fc6b2697cbdd866d8498 Mon Sep 17 00:00:00 2001 From: Bradley Cicenas Date: Sun, 12 May 2019 20:23:29 +0000 Subject: [PATCH 8/9] add exec shortcut key to container menu --- menus.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/menus.go b/menus.go index 52d5caf..5c893a6 100644 --- a/menus.go +++ b/menus.go @@ -135,7 +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"}) + items = append(items, menu.Item{Val: "exec", Label: "[e] exec shell"}) } if c.Meta["state"] == "exited" || c.Meta["state"] == "created" { items = append(items, menu.Item{Val: "start", Label: "[s] start"}) @@ -184,6 +184,10 @@ func ContainerMenu() MenuFn { }) } if c.Meta["state"] == "running" { + ui.Handle("/sys/kbd/e", func(ui.Event) { + selected = "exec" + ui.StopLoop() + }) ui.Handle("/sys/kbd/r", func(ui.Event) { selected = "restart" ui.StopLoop() @@ -212,7 +216,7 @@ func ContainerMenu() MenuFn { nextMenu = SingleView case "logs": nextMenu = LogMenu - case "exec shell": + case "exec": nextMenu = ExecShell case "start": nextMenu = Confirm(confirmTxt("start", c.GetMeta("name")), c.Start) From d187e8c623fea0006b4eb5d7c163f8b4b813c1eb Mon Sep 17 00:00:00 2001 From: Bradley Cicenas Date: Sun, 12 May 2019 20:23:54 +0000 Subject: [PATCH 9/9] drop potentially empty initial frames during exec attach --- connector/manager/docker.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/connector/manager/docker.go b/connector/manager/docker.go index 1e8a91b..5b683fc 100644 --- a/connector/manager/docker.go +++ b/connector/manager/docker.go @@ -22,11 +22,11 @@ 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 + io.Reader } func (w *noClosableReader) Read(p []byte) (n int, err error) { - return w.wrappedReader.Read(p) + return w.Reader.Read(p) } const ( @@ -35,9 +35,7 @@ const ( STDERR = 2 ) -var ( - wrongFrameFormat = errors.New("Wrong frame format") -) +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} @@ -54,6 +52,11 @@ type frameWriter struct { } func (w *frameWriter) Write(p []byte) (n int, err error) { + // drop initial empty frames + if len(p) == 0 { + return 0, nil + } + if len(p) > 8 { var targetWriter io.Writer switch p[0] {