diff --git a/Dockerfile b/Dockerfile
index 4617766..fab2a93 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM quay.io/vektorcloud/go:1.11
+FROM quay.io/vektorcloud/go:1.12
RUN apk add --no-cache make
diff --git a/README.md b/README.md
index 99811cc..9a56325 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your pla
#### Linux
```bash
-sudo wget https://github.com/bcicen/ctop/releases/download/v0.7.1/ctop-0.7.1-linux-amd64 -O /usr/local/bin/ctop
+sudo wget https://github.com/bcicen/ctop/releases/download/v0.7.2/ctop-0.7.2-linux-amd64 -O /usr/local/bin/ctop
sudo chmod +x /usr/local/bin/ctop
```
@@ -31,7 +31,7 @@ brew install ctop
```
or
```bash
-sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.7.1/ctop-0.7.1-darwin-amd64
+sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.7.2/ctop-0.7.2-darwin-amd64
sudo chmod +x /usr/local/bin/ctop
```
@@ -40,7 +40,7 @@ sudo chmod +x /usr/local/bin/ctop
```bash
docker run --rm -ti \
--name=ctop \
- -v /var/run/docker.sock:/var/run/docker.sock \
+ --volume /var/run/docker.sock:/var/run/docker.sock:ro \
quay.io/vektorlab/ctop:latest
```
@@ -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/VERSION b/VERSION
index 39e898a..7486fdb 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.7.1
+0.7.2
diff --git a/_docs/connectors.md b/_docs/connectors.md
index 893b334..9b3247e 100644
--- a/_docs/connectors.md
+++ b/_docs/connectors.md
@@ -1,4 +1,4 @@
-# connectors
+# Connectors
`ctop` comes with the below native connectors, enabled via the `--connector` option.
diff --git a/_docs/img/status.png b/_docs/img/status.png
new file mode 100644
index 0000000..86d03e8
Binary files /dev/null and b/_docs/img/status.png differ
diff --git a/_docs/status.md b/_docs/status.md
new file mode 100644
index 0000000..082a20c
--- /dev/null
+++ b/_docs/status.md
@@ -0,0 +1,30 @@
+# Status Indicator
+
+The `ctop` grid view provides a compact status indicator to convey container state
+
+
+
+### Status
+
+
+
+Appearance | Description
+--- | ---
+red | container is stopped
+green | container is running
+▮▮ | container is paused
+
+
+
+### Health
+If the container is configured with a health check, a `+` will appear next to the indicator
+
+
+
+Appearance | Description
+--- | ---
+red | health check in failed state
+yellow | health check in starting state
+green | health check in OK state
+
+
diff --git a/config/param.go b/config/param.go
index d954bdc..8c24e7c 100644
--- a/config/param.go
+++ b/config/param.go
@@ -17,6 +17,11 @@ var params = []*Param{
Val: "state",
Label: "Kubernetes namespace for monitoring",
},
+ &Param{
+ Key: "shell",
+ Val: "sh",
+ Label: "Shell",
+ },
}
type Param struct {
@@ -35,7 +40,7 @@ func Get(k string) *Param {
return &Param{} // default
}
-// Get Param value by key
+// GetVal gets Param value by key
func GetVal(k string) string {
return Get(k).Val
}
diff --git a/config/switch.go b/config/switch.go
index e209d79..ab3d7e0 100644
--- a/config/switch.go
+++ b/config/switch.go
@@ -35,7 +35,7 @@ type Switch struct {
Label string
}
-// Return Switch by key
+// GetSwitch returns Switch by key
func GetSwitch(k string) *Switch {
for _, sw := range GlobalSwitches {
if sw.Key == k {
@@ -45,7 +45,7 @@ func GetSwitch(k string) *Switch {
return &Switch{} // default
}
-// Return Switch value by key
+// GetSwitchVal returns Switch value by key
func GetSwitchVal(k string) bool {
return GetSwitch(k).Val
}
diff --git a/connector/docker.go b/connector/docker.go
index 73ea10d..6595059 100644
--- a/connector/docker.go
+++ b/connector/docker.go
@@ -17,27 +17,45 @@ type Docker struct {
client *api.Client
containers map[string]*container.Container
needsRefresh chan string // container IDs requiring refresh
+ closed chan struct{}
lock sync.RWMutex
}
-func NewDocker() Connector {
+func NewDocker() (Connector, error) {
// init docker client
client, err := api.NewClientFromEnv()
if err != nil {
- panic(err)
+ return nil, err
}
cm := &Docker{
client: client,
containers: make(map[string]*container.Container),
needsRefresh: make(chan string, 60),
+ closed: make(chan struct{}),
lock: sync.RWMutex{},
}
+
+ // query info as pre-flight healthcheck
+ info, err := client.Info()
+ if err != nil {
+ return nil, err
+ }
+
+ log.Debugf("docker-connector ID: %s", info.ID)
+ log.Debugf("docker-connector Driver: %s", info.Driver)
+ log.Debugf("docker-connector Images: %d", info.Images)
+ log.Debugf("docker-connector Name: %s", info.Name)
+ log.Debugf("docker-connector ServerVersion: %s", info.ServerVersion)
+
go cm.Loop()
cm.refreshAll()
go cm.watchEvents()
- return cm
+ return cm, nil
}
+// Docker implements Connector
+func (cm *Docker) Wait() struct{} { return <-cm.closed }
+
// Docker events watcher
func (cm *Docker) watchEvents() {
log.Info("docker event listener starting")
@@ -60,6 +78,8 @@ func (cm *Docker) watchEvents() {
cm.delByID(e.ID)
}
}
+ log.Info("docker event listener exited")
+ close(cm.closed)
}
func portsFormat(ports map[api.Port][]api.PortBinding) string {
@@ -114,7 +134,7 @@ func (cm *Docker) inspect(id string) *api.Container {
c, err := cm.client.InspectContainer(id)
if err != nil {
if _, ok := err.(*api.NoSuchContainer); !ok {
- log.Errorf(err.Error())
+ log.Errorf("%s (%T)", err.Error(), err)
}
}
return c
@@ -125,7 +145,8 @@ func (cm *Docker) refreshAll() {
opts := api.ListContainersOptions{All: true}
allContainers, err := cm.client.ListContainers(opts)
if err != nil {
- panic(err)
+ log.Errorf("%s (%T)", err.Error(), err)
+ return
}
for _, i := range allContainers {
@@ -137,13 +158,18 @@ func (cm *Docker) refreshAll() {
}
func (cm *Docker) Loop() {
- for id := range cm.needsRefresh {
- c := cm.MustGet(id)
- cm.refresh(c)
+ for {
+ select {
+ case id := <-cm.needsRefresh:
+ c := cm.MustGet(id)
+ cm.refresh(c)
+ case <-cm.closed:
+ return
+ }
}
}
-// Get a single container, creating one anew if not existing
+// MustGet gets a single container, creating one anew if not existing
func (cm *Docker) MustGet(id string) *container.Container {
c, ok := cm.Get(id)
// append container struct for new containers
@@ -161,7 +187,7 @@ func (cm *Docker) MustGet(id string) *container.Container {
return c
}
-// Get a single container, by ID
+// Docker implements Connector
func (cm *Docker) Get(id string) (*container.Container, bool) {
cm.lock.Lock()
c, ok := cm.containers[id]
@@ -177,7 +203,7 @@ func (cm *Docker) delByID(id string) {
log.Infof("removed dead container: %s", id)
}
-// Return array of all containers, sorted by field
+// Docker implements Connector
func (cm *Docker) All() (containers container.Containers) {
cm.lock.Lock()
for _, c := range cm.containers {
diff --git a/connector/main.go b/connector/main.go
index 9162d77..0907a77 100644
--- a/connector/main.go
+++ b/connector/main.go
@@ -3,6 +3,8 @@ package connector
import (
"fmt"
"sort"
+ "sync"
+ "time"
"github.com/bcicen/ctop/container"
"github.com/bcicen/ctop/logging"
@@ -10,10 +12,80 @@ import (
var (
log = logging.Init()
- enabled = make(map[string]func() Connector)
+ enabled = make(map[string]ConnectorFn)
)
-// return names for all enabled connectors on the current platform
+type ConnectorFn func() (Connector, error)
+
+type Connector interface {
+ // All returns a pre-sorted container.Containers of all discovered containers
+ All() container.Containers
+ // Get returns a single container.Container by ID
+ Get(string) (*container.Container, bool)
+ // Wait waits for the underlying connection to be lost before returning
+ Wait() struct{}
+}
+
+// ConnectorSuper provides initial connection and retry on failure for
+// an undlerying Connector type
+type ConnectorSuper struct {
+ conn Connector
+ connFn ConnectorFn
+ err error
+ lock sync.RWMutex
+}
+
+func NewConnectorSuper(connFn ConnectorFn) *ConnectorSuper {
+ cs := &ConnectorSuper{
+ connFn: connFn,
+ err: fmt.Errorf("connecting..."),
+ }
+ go cs.loop()
+ return cs
+}
+
+// Get returns the underlying Connector, or nil and an error
+// if the Connector is not yet initialized or is disconnected.
+func (cs *ConnectorSuper) Get() (Connector, error) {
+ cs.lock.RLock()
+ defer cs.lock.RUnlock()
+ if cs.err != nil {
+ return nil, cs.err
+ }
+ return cs.conn, nil
+}
+
+func (cs *ConnectorSuper) setError(err error) {
+ cs.lock.Lock()
+ defer cs.lock.Unlock()
+ cs.err = err
+}
+
+func (cs *ConnectorSuper) loop() {
+ const interval = 3
+ for {
+ log.Infof("initializing connector")
+
+ conn, err := cs.connFn()
+ if err != nil {
+ cs.setError(err)
+ log.Errorf("failed to initialize connector: %s (%T)", err, err)
+ log.Errorf("retrying in %ds", interval)
+ time.Sleep(interval * time.Second)
+ } else {
+ cs.conn = conn
+ cs.setError(nil)
+ log.Infof("successfully initialized connector")
+
+ // wait until connection closed
+ cs.conn.Wait()
+ cs.setError(fmt.Errorf("attempting to reconnect..."))
+ log.Infof("connector closed")
+ }
+ }
+}
+
+// Enabled returns names for all enabled connectors on the current platform
func Enabled() (a []string) {
for k, _ := range enabled {
a = append(a, k)
@@ -22,14 +94,11 @@ func Enabled() (a []string) {
return a
}
-func ByName(s string) (Connector, error) {
+// ByName returns a ConnectorSuper for a given name, or error if the connector
+// does not exists on the current platform
+func ByName(s string) (*ConnectorSuper, error) {
if cfn, ok := enabled[s]; ok {
- return cfn(), nil
+ return NewConnectorSuper(cfn), nil
}
return nil, fmt.Errorf("invalid connector type \"%s\"", s)
}
-
-type Connector interface {
- All() container.Containers
- Get(string) (*container.Container, bool)
-}
diff --git a/connector/manager/docker.go b/connector/manager/docker.go
index 77dc987..5b683fc 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,88 @@ 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 {
+ io.Reader
+}
+
+func (w *noClosableReader) Read(p []byte) (n int, err error) {
+ return w.Reader.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) {
+ // drop initial empty frames
+ if len(p) == 0 {
+ return 0, nil
+ }
+
+ 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/connector/mock.go b/connector/mock.go
index bfa30d1..d96496b 100644
--- a/connector/mock.go
+++ b/connector/mock.go
@@ -20,11 +20,11 @@ type Mock struct {
containers container.Containers
}
-func NewMock() Connector {
+func NewMock() (Connector, error) {
cs := &Mock{}
go cs.Init()
go cs.Loop()
- return cs
+ return cs, nil
}
// Create Mock containers
@@ -41,6 +41,15 @@ func (cs *Mock) Init() {
}
+func (cs *Mock) Wait() struct{} {
+ ch := make(chan struct{})
+ go func() {
+ time.Sleep(30 * time.Second)
+ close(ch)
+ }()
+ return <-ch
+}
+
func (cs *Mock) makeContainer(aggression int64) {
collector := collector.NewMock(aggression)
manager := manager.NewMock()
@@ -73,7 +82,7 @@ func (cs *Mock) Get(id string) (*container.Container, bool) {
return nil, false
}
-// Return array of all containers, sorted by field
+// All returns array of all containers, sorted by field
func (cs *Mock) All() container.Containers {
cs.containers.Sort()
cs.containers.Filter()
diff --git a/connector/runc.go b/connector/runc.go
index 796e2f0..c9f7c87 100644
--- a/connector/runc.go
+++ b/connector/runc.go
@@ -54,35 +54,44 @@ type Runc struct {
factory libcontainer.Factory
containers map[string]*container.Container
libContainers map[string]libcontainer.Container
+ closed chan struct{}
needsRefresh chan string // container IDs requiring refresh
lock sync.RWMutex
}
-func NewRunc() Connector {
+func NewRunc() (Connector, error) {
opts, err := NewRuncOpts()
- runcFailOnErr(err)
+ if err != nil {
+ return nil, err
+ }
factory, err := getFactory(opts)
- runcFailOnErr(err)
+ if err != nil {
+ return nil, err
+ }
cm := &Runc{
opts: opts,
factory: factory,
containers: make(map[string]*container.Container),
libContainers: make(map[string]libcontainer.Container),
- needsRefresh: make(chan string, 60),
+ closed: make(chan struct{}),
lock: sync.RWMutex{},
}
go func() {
for {
- cm.refreshAll()
- time.Sleep(5 * time.Second)
+ select {
+ case <-cm.closed:
+ return
+ case <-time.After(5 * time.Second):
+ cm.refreshAll()
+ }
}
}()
go cm.Loop()
- return cm
+ return cm, nil
}
func (cm *Runc) GetLibc(id string) libcontainer.Container {
@@ -141,7 +150,11 @@ func (cm *Runc) refresh(id string) {
// Read runc root, creating any new containers
func (cm *Runc) refreshAll() {
list, err := ioutil.ReadDir(cm.opts.root)
- runcFailOnErr(err)
+ if err != nil {
+ log.Errorf("%s (%T)", err.Error(), err)
+ close(cm.closed)
+ return
+ }
for _, i := range list {
if i.IsDir() {
@@ -168,7 +181,7 @@ func (cm *Runc) Loop() {
}
}
-// Get a single ctop container in the map matching libc container, creating one anew if not existing
+// MustGet gets a single ctop container in the map matching libc container, creating one anew if not existing
func (cm *Runc) MustGet(id string) *container.Container {
c, ok := cm.Get(id)
if !ok {
@@ -199,14 +212,6 @@ func (cm *Runc) MustGet(id string) *container.Container {
return c
}
-// Get a single container, by ID
-func (cm *Runc) Get(id string) (*container.Container, bool) {
- cm.lock.Lock()
- defer cm.lock.Unlock()
- c, ok := cm.containers[id]
- return c, ok
-}
-
// Remove containers by ID
func (cm *Runc) delByID(id string) {
cm.lock.Lock()
@@ -216,7 +221,18 @@ func (cm *Runc) delByID(id string) {
log.Infof("removed dead container: %s", id)
}
-// Return array of all containers, sorted by field
+// Runc implements Connector
+func (cm *Runc) Wait() struct{} { return <-cm.closed }
+
+// Runc implements Connector
+func (cm *Runc) Get(id string) (*container.Container, bool) {
+ cm.lock.Lock()
+ defer cm.lock.Unlock()
+ c, ok := cm.containers[id]
+ return c, ok
+}
+
+// Runc implements Connector
func (cm *Runc) All() (containers container.Containers) {
cm.lock.Lock()
for _, c := range cm.containers {
@@ -239,9 +255,3 @@ func getFactory(opts RuncOpts) (libcontainer.Factory, error) {
}
return libcontainer.New(opts.root, cgroupManager)
}
-
-func runcFailOnErr(err error) {
- if err != nil {
- panic(fmt.Errorf("fatal runc error: %s", err))
- }
-}
diff --git a/container/main.go b/container/main.go
index b4bd0b3..416b878 100644
--- a/container/main.go
+++ b/container/main.go
@@ -74,7 +74,7 @@ func (c *Container) SetState(s string) {
}
}
-// Return container log collector
+// Logs returns container log collector
func (c *Container) Logs() collector.LogCollector {
return c.collector.Logs()
}
@@ -153,3 +153,7 @@ func (c *Container) Restart() {
}
}
}
+
+func (c *Container) Exec(cmd []string) error {
+ return c.manager.Exec(cmd)
+}
diff --git a/cursor.go b/cursor.go
index aaa1ec9..6c5a6e8 100644
--- a/cursor.go
+++ b/cursor.go
@@ -11,7 +11,7 @@ import (
type GridCursor struct {
selectedID string // id of currently selected container
filtered container.Containers
- cSource connector.Connector
+ cSuper *connector.ConnectorSuper
isScrolling bool // toggled when actively scrolling
}
@@ -25,14 +25,20 @@ func (gc *GridCursor) Selected() *container.Container {
return nil
}
-// Refresh containers from source
-func (gc *GridCursor) RefreshContainers() (lenChanged bool) {
+// Refresh containers from source, returning whether the quantity of
+// containers has changed and any error
+func (gc *GridCursor) RefreshContainers() (bool, error) {
oldLen := gc.Len()
-
- // Containers filtered by display bool
gc.filtered = container.Containers{}
+
+ cSource, err := gc.cSuper.Get()
+ if err != nil {
+ return true, err
+ }
+
+ // filter Containers by display bool
var cursorVisible bool
- for _, c := range gc.cSource.All() {
+ for _, c := range cSource.All() {
if c.Display {
if c.Id == gc.selectedID {
cursorVisible = true
@@ -41,22 +47,21 @@ func (gc *GridCursor) RefreshContainers() (lenChanged bool) {
}
}
- if oldLen != gc.Len() {
- lenChanged = true
- }
-
- if !cursorVisible {
+ if !cursorVisible || gc.selectedID == "" {
gc.Reset()
}
- if gc.selectedID == "" {
- gc.Reset()
- }
- return lenChanged
+
+ return oldLen != gc.Len(), nil
}
// Set an initial cursor position, if possible
func (gc *GridCursor) Reset() {
- for _, c := range gc.cSource.All() {
+ cSource, err := gc.cSuper.Get()
+ if err != nil {
+ return
+ }
+
+ for _, c := range cSource.All() {
c.Widgets.UnHighlight()
}
if gc.Len() > 0 {
@@ -65,7 +70,7 @@ func (gc *GridCursor) Reset() {
}
}
-// Return current cursor index
+// Idx returns current cursor index
func (gc *GridCursor) Idx() int {
for n, c := range gc.filtered {
if c.Id == gc.selectedID {
diff --git a/cwidgets/compact/status.go b/cwidgets/compact/status.go
index 07fb272..edc873c 100644
--- a/cwidgets/compact/status.go
+++ b/cwidgets/compact/status.go
@@ -5,8 +5,8 @@ import (
)
const (
- mark = string('\u25C9')
- healthMark = string('\u207A')
+ mark = "◉"
+ healthMark = "✚"
vBar = string('\u25AE') + string('\u25AE')
)
@@ -18,7 +18,10 @@ type Status struct {
}
func NewStatus() *Status {
- s := &Status{Block: ui.NewBlock()}
+ s := &Status{
+ Block: ui.NewBlock(),
+ health: []ui.Cell{{Ch: ' '}},
+ }
s.Height = 1
s.Border = false
s.Set("")
@@ -28,11 +31,12 @@ func NewStatus() *Status {
func (s *Status) Buffer() ui.Buffer {
buf := s.Block.Buffer()
x := 0
- for _, c := range s.status {
+ for _, c := range s.health {
buf.Set(s.InnerX()+x, s.InnerY(), c)
x += c.Width()
}
- for _, c := range s.health {
+ x += 1
+ for _, c := range s.status {
buf.Set(s.InnerX()+x, s.InnerY(), c)
x += c.Width()
}
@@ -53,18 +57,16 @@ func (s *Status) Set(val string) {
text = vBar
}
- var cells []ui.Cell
- for _, ch := range text {
- cells = append(cells, ui.Cell{Ch: ch, Fg: color})
- }
- s.status = cells
+ s.status = ui.TextCells(text, color, ui.ColorDefault)
}
func (s *Status) SetHealth(val string) {
if val == "" {
return
}
+
color := ui.ColorDefault
+ mark := healthMark
switch val {
case "healthy", "Succeeded":
@@ -75,9 +77,5 @@ func (s *Status) SetHealth(val string) {
color = ui.ThemeAttr("status.warn")
}
- var cells []ui.Cell
- for _, ch := range healthMark {
- cells = append(cells, ui.Cell{Ch: ch, Fg: color})
- }
- s.health = cells
+ s.health = ui.TextCells(mark, color, ui.ColorDefault)
}
diff --git a/cwidgets/compact/util.go b/cwidgets/compact/util.go
index e634ed9..9bd8f66 100644
--- a/cwidgets/compact/util.go
+++ b/cwidgets/compact/util.go
@@ -12,7 +12,7 @@ const colSpacing = 1
// per-column width. 0 == auto width
var colWidths = []int{
- 3, // status
+ 5, // status
0, // name
0, // cid
0, // cpu
diff --git a/cwidgets/single/main.go b/cwidgets/single/main.go
index 934e212..8140b53 100644
--- a/cwidgets/single/main.go
+++ b/cwidgets/single/main.go
@@ -70,7 +70,7 @@ func (e *Single) SetMetrics(m models.Metrics) {
e.IO.Update(m.IOBytesRead, m.IOBytesWrite)
}
-// Return total column height
+// GetHeight returns total column height
func (e *Single) GetHeight() (h int) {
h += e.Info.Height
h += e.Net.Height
diff --git a/go.mod b/go.mod
index 1d36d0f..56db41f 100644
--- a/go.mod
+++ b/go.mod
@@ -1,13 +1,12 @@
module github.com/bcicen/ctop
require (
- github.com/Azure/go-ansiterm v0.0.0-20160622173216-fa152c58bc15 // indirect
github.com/BurntSushi/toml v0.3.0
- github.com/Microsoft/go-winio v0.3.8 // indirect
- github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
- github.com/Sirupsen/logrus v0.0.0-20150423025312-26709e271410 // indirect
github.com/c9s/goprocinfo v0.0.0-20170609001544-b34328d6e0cd
+ github.com/checkpoint-restore/go-criu v0.0.0-20190109184317-bdb7599cd87b // indirect
+ github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50 // indirect
github.com/coreos/go-systemd v0.0.0-20151104194251-b4a58d95188d // indirect
+<<<<<<< HEAD
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/docker v0.0.0-20170502054910-90d35abf7b35 // indirect
github.com/docker/go-connections v0.0.0-20170301234100-a2afab980204 // indirect
@@ -56,6 +55,12 @@ require (
k8s.io/klog v0.1.0 // indirect
k8s.io/metrics v0.0.0-20181121073115-d8618695b08f
sigs.k8s.io/yaml v1.1.0 // indirect
+ github.com/cyphar/filepath-securejoin v0.2.2 // indirect
+ github.com/mrunalp/fileutils v0.0.0-20171103030105-7d4729fb3618 // indirect
+ github.com/opencontainers/runtime-spec v1.0.1 // indirect
+ github.com/opencontainers/selinux v1.2.2 // indirect
+ github.com/pkg/errors v0.8.1
+ github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 // indirect
)
replace github.com/gizak/termui => github.com/bcicen/termui v0.0.0-20180326052246-4eb80249d3f5
diff --git a/grid.go b/grid.go
index 501c216..655e000 100644
--- a/grid.go
+++ b/grid.go
@@ -6,6 +6,44 @@ import (
ui "github.com/gizak/termui"
)
+func ShowConnError(err error) (exit bool) {
+ ui.Clear()
+ ui.DefaultEvtStream.ResetHandlers()
+ defer ui.DefaultEvtStream.ResetHandlers()
+
+ setErr := func(err error) {
+ errView.Append(err.Error())
+ errView.Append("attempting to reconnect...")
+ ui.Render(errView)
+ }
+
+ HandleKeys("exit", func() {
+ exit = true
+ ui.StopLoop()
+ })
+
+ ui.Handle("/timer/1s", func(ui.Event) {
+ _, err := cursor.RefreshContainers()
+ if err == nil {
+ ui.StopLoop()
+ return
+ }
+ setErr(err)
+ })
+
+ ui.Handle("/sys/wnd/resize", func(e ui.Event) {
+ errView.Resize()
+ ui.Clear()
+ ui.Render(errView)
+ log.Infof("RESIZE")
+ })
+
+ errView.Resize()
+ setErr(err)
+ ui.Loop()
+ return exit
+}
+
func RedrawRows(clr bool) {
// reinit body rows
cGrid.Clear()
@@ -33,7 +71,6 @@ func RedrawRows(clr bool) {
}
cGrid.Align()
ui.Render(cGrid)
-
}
func SingleView() MenuFn {
@@ -68,16 +105,21 @@ func SingleView() MenuFn {
return nil
}
-func RefreshDisplay() {
+func RefreshDisplay() error {
// skip display refresh during scroll
if !cursor.isScrolling {
- needsClear := cursor.RefreshContainers()
+ needsClear, err := cursor.RefreshContainers()
+ if err != nil {
+ return err
+ }
RedrawRows(needsClear)
}
+ return nil
}
func Display() bool {
var menu MenuFn
+ var connErr error
cGrid.SetWidth(ui.TermWidth())
ui.DefaultEvtStream.Hook(logEvent)
@@ -116,13 +158,20 @@ 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()
})
ui.Handle("/sys/kbd/a", func(ui.Event) {
config.Toggle("allContainers")
- RefreshDisplay()
+ connErr = RefreshDisplay()
+ if connErr != nil {
+ ui.StopLoop()
+ }
})
ui.Handle("/sys/kbd/D", func(ui.Event) {
dumpContainer(cursor.Selected())
@@ -156,7 +205,10 @@ func Display() bool {
if log.StatusQueued() {
ui.StopLoop()
}
- RefreshDisplay()
+ connErr = RefreshDisplay()
+ if connErr != nil {
+ ui.StopLoop()
+ }
})
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
@@ -170,6 +222,10 @@ func Display() bool {
ui.Loop()
+ if connErr != nil {
+ return ShowConnError(connErr)
+ }
+
if log.StatusQueued() {
for sm := range log.FlushStatus() {
if sm.IsError {
diff --git a/main.go b/main.go
index ef0f18a..bd653fe 100644
--- a/main.go
+++ b/main.go
@@ -22,11 +22,12 @@ var (
version = "dev-build"
goVersion = runtime.Version()
- log *logging.CTopLogger
- cursor *GridCursor
- cGrid *compact.CompactGrid
- header *widgets.CTopHeader
- status *widgets.StatusLine
+ log *logging.CTopLogger
+ cursor *GridCursor
+ cGrid *compact.CompactGrid
+ header *widgets.CTopHeader
+ status *widgets.StatusLine
+ errView *widgets.ErrorView
versionStr = fmt.Sprintf("ctop version %v, build %v %v", version, build, goVersion)
@@ -87,6 +88,10 @@ func main() {
config.Toggle("scaleCpu")
}
+ if *defaultShell != "" {
+ config.Update("shell", *defaultShell)
+ }
+
// init ui
if *invertFlag {
InvertColorMap()
@@ -100,14 +105,15 @@ func main() {
defer Shutdown()
// init grid, cursor, header
- conn, err := connector.ByName(*connectorFlag)
+ cSuper, err := connector.ByName(*connectorFlag)
if err != nil {
panic(err)
}
- cursor = &GridCursor{cSource: conn}
+ cursor = &GridCursor{cSuper: cSuper}
cGrid = compact.NewCompactGrid()
header = widgets.NewCTopHeader()
status = widgets.NewStatusLine()
+ errView = widgets.NewErrorView()
for {
exit := Display()
@@ -136,6 +142,7 @@ func validSort(s string) {
func panicExit() {
if r := recover(); r != nil {
Shutdown()
+ panic(r)
fmt.Printf("error: %s\n", r)
os.Exit(1)
}
diff --git a/menus.go b/menus.go
index 0c819c7..16c18b7 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", ""},
}
@@ -126,55 +127,111 @@ func ContainerMenu() MenuFn {
m.BorderLabel = "Menu"
items := []menu.Item{
- menu.Item{Val: "single", Label: "single view"},
- menu.Item{Val: "logs", Label: "log view"},
+ menu.Item{Val: "single", Label: "[o] single view"},
+ menu.Item{Val: "logs", Label: "[l] log view"},
}
if c.Meta["state"] == "running" {
- 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: "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", Label: "[e] exec shell"})
}
if c.Meta["state"] == "exited" || c.Meta["state"] == "created" {
- items = append(items, menu.Item{Val: "start", Label: "start"})
- items = append(items, menu.Item{Val: "remove", Label: "remove"})
+ items = append(items, menu.Item{Val: "start", Label: "[s] start"})
+ items = append(items, menu.Item{Val: "remove", Label: "[R] remove"})
}
if c.Meta["state"] == "paused" {
- items = append(items, menu.Item{Val: "unpause", Label: "unpause"})
+ items = append(items, menu.Item{Val: "unpause", Label: "[p] unpause"})
}
- items = append(items, menu.Item{Val: "cancel", Label: "cancel"})
+ items = append(items, menu.Item{Val: "cancel", Label: "[c] cancel"})
m.AddItems(items...)
ui.Render(m)
- var nextMenu MenuFn
HandleKeys("up", m.Up)
HandleKeys("down", m.Down)
+
+ var selected string
+
+ // shortcuts
+ ui.Handle("/sys/kbd/o", func(ui.Event) {
+ selected = "single"
+ ui.StopLoop()
+ })
+ ui.Handle("/sys/kbd/l", func(ui.Event) {
+ selected = "logs"
+ ui.StopLoop()
+ })
+ if c.Meta["state"] != "paused" {
+ ui.Handle("/sys/kbd/s", func(ui.Event) {
+ if c.Meta["state"] == "running" {
+ selected = "stop"
+ } else {
+ selected = "start"
+ }
+ ui.StopLoop()
+ })
+ }
+ if c.Meta["state"] != "exited" || c.Meta["state"] != "created" {
+ ui.Handle("/sys/kbd/p", func(ui.Event) {
+ if c.Meta["state"] == "paused" {
+ selected = "unpause"
+ } else {
+ selected = "pause"
+ }
+ ui.StopLoop()
+ })
+ }
+ 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()
+ })
+ }
+ ui.Handle("/sys/kbd/R", func(ui.Event) {
+ selected = "remove"
+ ui.StopLoop()
+ })
+ ui.Handle("/sys/kbd/c", func(ui.Event) {
+ ui.StopLoop()
+ })
+
ui.Handle("/sys/kbd/", func(ui.Event) {
- switch m.SelectedItem().Val {
- case "single":
- nextMenu = SingleView
- case "logs":
- nextMenu = LogMenu
- case "start":
- nextMenu = Confirm(confirmTxt("start", c.GetMeta("name")), c.Start)
- case "stop":
- nextMenu = Confirm(confirmTxt("stop", c.GetMeta("name")), c.Stop)
- case "remove":
- nextMenu = Confirm(confirmTxt("remove", c.GetMeta("name")), c.Remove)
- case "pause":
- nextMenu = Confirm(confirmTxt("pause", c.GetMeta("name")), c.Pause)
- case "unpause":
- nextMenu = Confirm(confirmTxt("unpause", c.GetMeta("name")), c.Unpause)
- case "restart":
- nextMenu = Confirm(confirmTxt("restart", c.GetMeta("name")), c.Restart)
- }
+ selected = m.SelectedItem().Val
ui.StopLoop()
})
ui.Handle("/sys/kbd/", func(ui.Event) {
ui.StopLoop()
})
ui.Loop()
+
+ var nextMenu MenuFn
+ switch selected {
+ case "single":
+ nextMenu = SingleView
+ case "logs":
+ nextMenu = LogMenu
+ case "exec":
+ nextMenu = ExecShell
+ case "start":
+ nextMenu = Confirm(confirmTxt("start", c.GetMeta("name")), c.Start)
+ case "stop":
+ nextMenu = Confirm(confirmTxt("stop", c.GetMeta("name")), c.Stop)
+ case "remove":
+ nextMenu = Confirm(confirmTxt("remove", c.GetMeta("name")), c.Remove)
+ case "pause":
+ nextMenu = Confirm(confirmTxt("pause", c.GetMeta("name")), c.Pause)
+ case "unpause":
+ nextMenu = Confirm(confirmTxt("unpause", c.GetMeta("name")), c.Unpause)
+ case "restart":
+ nextMenu = Confirm(confirmTxt("restart", c.GetMeta("name")), c.Restart)
+ }
+
return nextMenu
}
@@ -207,6 +264,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", "printf '\\e[0m\\e[?25h' && 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 {
diff --git a/prepare-minikube.sh b/prepare-minikube.sh
new file mode 100644
index 0000000..ae64a59
--- /dev/null
+++ b/prepare-minikube.sh
@@ -0,0 +1,6 @@
+#/bin/sh
+
+curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 \
+ && chmod +x minikube
+
+sudo install minikube /usr/local/bin
diff --git a/widgets/error.go b/widgets/error.go
new file mode 100644
index 0000000..7f6a4a9
--- /dev/null
+++ b/widgets/error.go
@@ -0,0 +1,60 @@
+package widgets
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ ui "github.com/gizak/termui"
+)
+
+type ErrorView struct {
+ *ui.Par
+ lines []string
+}
+
+func NewErrorView() *ErrorView {
+ const yPad = 1
+ const xPad = 2
+
+ p := ui.NewPar("")
+ p.X = xPad
+ p.Y = yPad
+ p.Border = true
+ p.Height = 10
+ p.Width = 20
+ p.PaddingTop = yPad
+ p.PaddingBottom = yPad
+ p.PaddingLeft = xPad
+ p.PaddingRight = xPad
+ p.BorderLabel = " ctop - error "
+ p.Bg = ui.ThemeAttr("bg")
+ p.TextFgColor = ui.ThemeAttr("status.warn")
+ p.TextBgColor = ui.ThemeAttr("menu.text.bg")
+ p.BorderFg = ui.ThemeAttr("status.warn")
+ p.BorderLabelFg = ui.ThemeAttr("status.warn")
+ return &ErrorView{p, make([]string, 0, 50)}
+}
+
+func (w *ErrorView) Append(s string) {
+ if len(w.lines)+2 >= cap(w.lines) {
+ w.lines = append(w.lines[:0], w.lines[2:]...)
+ }
+ ts := time.Now().Local().Format("15:04:05 MST")
+ w.lines = append(w.lines, fmt.Sprintf("[%s] %s", ts, s))
+ w.lines = append(w.lines, "")
+}
+
+func (w *ErrorView) Buffer() ui.Buffer {
+ offset := len(w.lines) - w.InnerHeight()
+ if offset < 0 {
+ offset = 0
+ }
+ w.Text = strings.Join(w.lines[offset:len(w.lines)], "\n")
+ return w.Par.Buffer()
+}
+
+func (w *ErrorView) Resize() {
+ w.Height = ui.TermHeight() - (w.PaddingTop + w.PaddingBottom)
+ w.SetWidth(ui.TermWidth() - (w.PaddingLeft + w.PaddingRight))
+}
diff --git a/widgets/header.go b/widgets/header.go
index 4a96f8f..a7ab786 100644
--- a/widgets/header.go
+++ b/widgets/header.go
@@ -16,7 +16,7 @@ type CTopHeader struct {
func NewCTopHeader() *CTopHeader {
return &CTopHeader{
- Time: headerPar(2, timeStr()),
+ Time: headerPar(2, ""),
Count: headerPar(24, "-"),
Filter: headerPar(40, ""),
bg: headerBg(),
diff --git a/widgets/menu/main.go b/widgets/menu/main.go
index a2e6f2a..4e4e83b 100644
--- a/widgets/menu/main.go
+++ b/widgets/menu/main.go
@@ -42,7 +42,7 @@ func (m *Menu) AddItems(items ...Item) {
m.refresh()
}
-// Remove menu item by value or label
+// DelItem removes menu item by value or label
func (m *Menu) DelItem(s string) (success bool) {
for n, i := range m.items {
if i.Val == s || i.Label == s {