Compare commits

..

36 Commits
k8s ... v0.7.3

Author SHA1 Message Date
Bradley Cicenas
4741b276e4 v0.7.3 2020-01-03 18:37:30 +00:00
bradley
9dd12103fc Merge pull request #186 from alexislefebvre/patch-1
Format options and keybindings as code
2019-12-30 08:31:49 -05:00
Alexis Lefebvre
c38942c7ed Format options and keybindings as code 2019-11-21 17:34:53 +01:00
Bradley Cicenas
4b391e900c use raw log stream in docker log collector 2019-11-06 07:32:29 -05:00
bradley
4584cf34f5 Merge pull request #178 from rvolosatovs/feature/go-sum
Add go.sum
2019-10-03 07:35:26 -04:00
Roman Volosatovs
1ce07448ce go.sum: Add 2019-09-10 20:22:25 +02:00
Bradley Cicenas
d8c7dd4c5c move image to go 1.12 2019-06-29 15:15:13 -04:00
Bradley Cicenas
b7d81485f9 update dockerclient, runc deps 2019-06-29 15:15:13 -04:00
bradley
8946c4b03b Merge pull request #169 from captn3m0/patch-1
Switches to a read-only socket mount
2019-06-29 14:32:08 -04:00
Bradley Cicenas
331f50f03e update status doc image 2019-06-22 18:44:07 +00:00
Bradley Cicenas
4c4f041b40 improve health check visibility 2019-06-22 18:42:48 +00:00
Bradley Cicenas
c8ac331652 fix timer 2019-05-22 17:39:37 +00:00
Bradley Cicenas
0a5a4c9062 add multi-line scrolling support, timestamps to error view 2019-05-22 17:38:01 +00:00
Bradley Cicenas
98fcfe8b6f refactor connectors for retry logic, add error view 2019-05-22 16:58:55 +00:00
bradley
42f095cd85 Merge pull request #170 from fr05t1k/exec
Enable cursor
2019-05-13 12:24:41 -04:00
Stanislav Pavlovichev
73986d2732 Enable cursor 2019-05-13 15:47:22 +03:00
Bradley Cicenas
c1d4615cc0 Merge branch 'fr05t1k-exec' 2019-05-12 16:26:51 -04:00
Bradley Cicenas
d187e8c623 drop potentially empty initial frames during exec attach 2019-05-12 20:23:54 +00:00
Bradley Cicenas
b8c38d09ef add exec shortcut key to container menu 2019-05-12 20:23:29 +00:00
Bradley Cicenas
d7384db373 Merge branch 'exec' of https://github.com/fr05t1k/ctop into fr05t1k-exec 2019-05-12 15:31:33 -04:00
Nemo
1b441db189 Switches to a read-only socket mount 2019-04-17 00:43:15 +05:30
bradley
0479d42e31 Merge pull request #168 from CodeLingoBot/rewrite
Fix function comments based on best practices from Effective Go
2019-03-07 09:41:02 +05:30
CodeLingo Bot
b401e7b17e Fix function comments based on best practices from Effective Go
Signed-off-by: CodeLingo Bot <bot@codelingo.io>
2019-03-07 02:33:29 +00:00
Bradley Cicenas
9592de82a0 add keyboard shortcuts to container menu 2019-01-24 14:06:40 +00:00
bradley
29fa8cf3e7 Update status.md 2019-01-24 08:02:52 -05:00
bradley
c49939f965 Update status.md 2019-01-24 08:02:13 -05:00
bradley
2f7bc2a172 Update status.md 2019-01-24 08:00:51 -05:00
Bradley Cicenas
7b4d4db049 add status indicator doc 2019-01-24 12:50:59 +00:00
Bradley Cicenas
70bd2ae3a3 v0.7.2 2019-01-24 11:50:49 +00:00
Stanislav Pavlovichev
101ddad692 Fixed a problem with rendering 2018-10-28 13:43:24 +02:00
Stanislav Pavlovichev
ca35ef2aab Unnecessary loop stopping 2018-10-28 12:07:43 +02:00
Stanislav Pavlovichev
d59c91a461 Do not allow to close /dev/stdin 2018-10-26 17:08:33 +03:00
Stanislav Pavlovichev
a26fc9169c Ability to change Shell 2018-10-25 21:58:33 +03:00
Stanislav Pavlovichev
967a87a65f Exec using API 2018-10-13 08:33:53 +03:00
Stanislav Pavlovichev
e68f7ba96a fix: handlers used to work after "exec sh" command
feature: hot key for "exec sh"
2018-10-12 10:03:27 +03:00
Stanislav Pavlovichev
f27de1c29e Exec Sh Feature 2018-10-07 16:46:32 +03:00
34 changed files with 760 additions and 712 deletions

View File

@@ -1,4 +1,4 @@
FROM quay.io/vektorcloud/go:1.11
FROM quay.io/vektorcloud/go:1.12
RUN apk add --no-cache make

View File

@@ -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.3/ctop-0.7.3-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.3/ctop-0.7.3-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
```
@@ -62,30 +62,32 @@ While running, use `S` to save the current filters, sort field, and other option
Option | Description
--- | ---
-a | show active containers only
-f \<string\> | set an initial filter string
-h | display help dialog
-i | invert default colors
-r | reverse container sort order
-s | select initial container sort field
-scale-cpu | show cpu as % of system total
-v | output version information and exit
`-a` | show active containers only
`-f <string>` | set an initial filter string
`-h` | display help dialog
`-i` | invert default colors
`-r` | reverse container sort order
`-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
Key | Action
--- | ---
\<enter\> | Open container menu
a | Toggle display of all (running and non-running) containers
f | Filter displayed containers (`esc` to clear when open)
H | Toggle ctop header
h | Open help dialog
s | Select container sort field
r | Reverse container sort order
o | Open single view
l | View container logs (`t` to toggle timestamp when open)
S | Save current configuration to file
q | Quit ctop
`<enter>` | Open container menu
`a` | Toggle display of all (running and non-running) containers
`f` | Filter displayed containers (`esc` to clear when open)
`H` | Toggle ctop header
`h` | Open help dialog
`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
[build]: _docs/build.md
[connectors]: _docs/connectors.md

View File

@@ -1 +1 @@
0.7.1
0.7.3

View File

@@ -1,4 +1,4 @@
# connectors
# Connectors
`ctop` comes with the below native connectors, enabled via the `--connector` option.

BIN
_docs/img/status.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

30
_docs/status.md Normal file
View File

@@ -0,0 +1,30 @@
# Status Indicator
The `ctop` grid view provides a compact status indicator to convey container state
<img width="200px" src="img/status.png" alt="ctop"/>
### Status
<span align="center">
Appearance | Description
--- | ---
red | container is stopped
green | container is running
▮▮ | container is paused
</span>
### Health
If the container is configured with a health check, a `+` will appear next to the indicator
<span align="center">
Appearance | Description
--- | ---
red | health check in failed state
yellow | health check in starting state
green | health check in OK state
</span>

View File

@@ -13,9 +13,9 @@ var params = []*Param{
Label: "Container Sort Field",
},
&Param{
Key: "namespace",
Val: "state",
Label: "Kubernetes namespace for monitoring",
Key: "shell",
Val: "sh",
Label: "Shell",
},
}
@@ -35,7 +35,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
}

View File

@@ -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
}

View File

@@ -34,12 +34,13 @@ func (l *DockerLogs) Stream() chan models.Log {
Context: ctx,
Container: l.id,
OutputStream: w,
ErrorStream: w,
//ErrorStream: w,
Stdout: true,
Stderr: true,
Tail: "10",
Tail: "20",
Follow: true,
Timestamps: true,
RawTerminal: true,
}
// read io pipe into channel
@@ -74,9 +75,25 @@ func (l *DockerLogs) Stop() { l.done <- true }
func (l *DockerLogs) parseTime(s string) time.Time {
ts, err := time.Parse("2006-01-02T15:04:05.000000000Z", s)
if err != nil {
log.Errorf("failed to parse container log: %s", err)
ts = time.Now()
}
if err == nil {
return ts
}
ts, err2 := time.Parse("2006-01-02T15:04:05.000000000Z", l.stripPfx(s))
if err2 == nil {
return ts
}
log.Errorf("failed to parse container log: %s", err)
log.Errorf("failed to parse container log2: %s", err2)
return time.Now()
}
// attempt to strip message header prefix from a given raw docker log string
func (l *DockerLogs) stripPfx(s string) string {
b := []byte(s)
if len(b) > 8 {
return string(b[8:])
}
return s
}

View File

@@ -1,128 +0,0 @@
package collector
import (
"time"
"k8s.io/metrics/pkg/apis/metrics/v1alpha1"
clientset "k8s.io/metrics/pkg/client/clientset/versioned"
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/models"
"k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
)
// Kubernetes collector
type Kubernetes struct {
models.Metrics
name string
client clientset.Interface
clientset *kubernetes.Clientset
running bool
stream chan models.Metrics
done chan bool
lastCpu float64
lastSysCpu float64
scaleCpu bool
}
func NewKubernetes(client *kubernetes.Clientset, name string) *Kubernetes {
return &Kubernetes{
Metrics: models.Metrics{},
name: name,
client: clientset.New(client.RESTClient()),
clientset: client,
scaleCpu: config.GetSwitchVal("scaleCpu"),
}
}
func (k *Kubernetes) Start() {
k.done = make(chan bool)
k.stream = make(chan models.Metrics)
go func() {
k.running = false
for {
result := &v1alpha1.PodMetrics{}
err := k.clientset.RESTClient().Get().AbsPath("/api/v1/namespaces/kube-system/services/http:heapster:/proxy/apis/metrics/v1alpha1/namespaces/" + config.GetVal("namespace") + "/pods/" + k.name).Do().Into(result)
if err != nil {
log.Errorf("has error %s here %s", k.name, err.Error())
time.Sleep(1 * time.Second)
continue
}
k.ReadCPU(result)
k.ReadMem(result)
k.stream <- k.Metrics
}
}()
k.running = true
log.Infof("collector started for container: %s", k.name)
}
func (c *Kubernetes) Running() bool {
return c.running
}
func (c *Kubernetes) Stream() chan models.Metrics {
return c.stream
}
func (c *Kubernetes) Logs() LogCollector {
return NewKubernetesLogs(c.name, c.clientset)
}
// Stop collector
func (c *Kubernetes) Stop() {
c.done <- true
}
func (k *Kubernetes) ReadCPU(metrics *v1alpha1.PodMetrics) {
all := int64(0)
for _, c := range metrics.Containers {
v := c.Usage[v1.ResourceCPU]
all += v.Value()
}
if all != 0 {
k.CPUUtil = round(float64(all))
}
}
func (k *Kubernetes) ReadMem(metrics *v1alpha1.PodMetrics) {
all := int64(0)
for _, c := range metrics.Containers {
v := c.Usage[v1.ResourceMemory]
a, ok := v.AsInt64()
if ok {
all += a
}
}
k.MemUsage = all
k.MemLimit = int64(0)
//k.MemPercent = percent(float64(k.MemUsage), float64(k.MemLimit))
}
//func (c *Kubernetes) ReadNet(stats *api.Stats) {
// var rx, tx int64
// for _, network := range stats.Networks {
// rx += int64(network.RxBytes)
// tx += int64(network.TxBytes)
// }
// c.NetRx, c.NetTx = rx, tx
//}
//
//func (c *Kubernetes) ReadIO(stats *api.Stats) {
// var read, write int64
// for _, blk := range stats.BlkioStats.IOServiceBytesRecursive {
// if blk.Op == "Read" {
// read = int64(blk.Value)
// }
// if blk.Op == "Write" {
// write = int64(blk.Value)
// }
// }
// c.IOBytesRead, c.IOBytesWrite = read, write
//}

View File

@@ -1,78 +0,0 @@
package collector
import (
"time"
"github.com/bcicen/ctop/models"
"k8s.io/client-go/kubernetes"
)
type KubernetesLogs struct {
id string
client *kubernetes.Clientset
done chan bool
}
func NewKubernetesLogs(id string, client *kubernetes.Clientset) *KubernetesLogs {
return &KubernetesLogs{
id: id,
client: client,
done: make(chan bool),
}
}
func (l *KubernetesLogs) Stream() chan models.Log {
//r, w := io.Pipe()
logCh := make(chan models.Log)
//ctx, cancel := context.WithCancel(context.Background())
//opts := api.LogsOptions{
// Context: ctx,
// Container: l.id,
// OutputStream: w,
// ErrorStream: w,
// Stdout: true,
// Stderr: true,
// Tail: "10",
// Follow: true,
// Timestamps: true,
//}
//// read io pipe into channel
//go func() {
// scanner := bufio.NewScanner(r)
// for scanner.Scan() {
// parts := strings.Split(scanner.Text(), " ")
// ts := l.parseTime(parts[0])
// logCh <- models.Log{Timestamp: ts, Message: strings.Join(parts[1:], " ")}
// }
//}()
//// connect to container log stream
//go func() {
// err := l.client.Logs(opts)
// if err != nil {
// log.Errorf("error reading container logs: %s", err)
// }
// log.Infof("log reader stopped for container: %s", l.id)
//}()
//go func() {
// <-l.done
// cancel()
//}()
log.Infof("log reader started for container: %s", l.id)
return logCh
}
func (l *KubernetesLogs) Stop() { l.done <- true }
func (l *KubernetesLogs) parseTime(s string) time.Time {
ts, err := time.Parse("2006-01-02T15:04:05.000000000Z", s)
if err != nil {
log.Errorf("failed to parse container log: %s", err)
ts = time.Now()
}
return ts
}

View File

@@ -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 {
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 {

View File

@@ -1,218 +0,0 @@
package connector
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
bcfg "github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/connector/collector"
"github.com/bcicen/ctop/connector/manager"
"github.com/bcicen/ctop/container"
api "github.com/fsouza/go-dockerclient"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
func init() { enabled["kubernetes"] = NewKubernetes }
type Kubernetes struct {
namespace string
clientset *kubernetes.Clientset
containers map[string]*container.Container
needsRefresh chan string // container IDs requiring refresh
lock sync.RWMutex
}
func NewKubernetes() Connector {
var kubeconfig string
//if home := homeDir(); home != "" {
// kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
//} else {
// kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
//}
//flag.Parse()
kubeconfig = filepath.Join(homeDir(), ".kube", "config")
// use the current context in kubeconfig
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
log.Error(err.Error())
return nil
}
// create the clientset
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
log.Error(err.Error())
return nil
}
// init docker client
k := &Kubernetes{
clientset: clientset,
containers: make(map[string]*container.Container),
needsRefresh: make(chan string, 60),
lock: sync.RWMutex{},
namespace: bcfg.GetVal("namespace"),
}
go k.Loop()
k.refreshAll()
go k.watchEvents()
return k
}
func (k *Kubernetes) watchEvents() {
for {
log.Info("kubernetes event listener starting")
allEvents, err := k.clientset.CoreV1().Events(k.namespace).List(metav1.ListOptions{})
if err != nil {
log.Error(err.Error())
return
}
for _, e := range allEvents.Items {
if e.Kind != "pod" {
continue
}
actionName := strings.Split(e.Action, ":")[0]
switch actionName {
case "start", "die", "pause", "unpause", "health_status":
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.UID)
k.needsRefresh <- e.Name
case "destroy":
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.UID)
k.delByID(e.Name)
default:
log.Debugf("handling docker event: %v", e)
k.needsRefresh <- e.Name
}
}
time.Sleep(1 * time.Second)
}
}
func (k *Kubernetes) Loop() {
for id := range k.needsRefresh {
c := k.MustGet(id)
k.refresh(c)
}
}
// Get a single container, creating one anew if not existing
func (k *Kubernetes) MustGet(name string) *container.Container {
c, ok := k.Get(name)
// append container struct for new containers
if !ok {
// create collector
collector := collector.NewKubernetes(k.clientset, name)
// create manager
manager := manager.NewKubernetes(k.clientset, name)
// create container
c = container.New(name, collector, manager)
k.lock.Lock()
k.containers[name] = c
k.lock.Unlock()
}
return c
}
func (k *Kubernetes) refresh(c *container.Container) {
insp := k.inspect(c.Id)
// remove container if no longer exists
if insp == nil {
k.delByID(c.Id)
return
}
c.SetMeta("name", insp.Name)
if len(insp.Spec.Containers) >= 1 {
c.SetMeta("image", insp.Spec.Containers[0].Image)
c.SetMeta("ports", k8sPort(insp.Spec.Containers[0].Ports))
for _, env := range insp.Spec.Containers[0].Env {
c.SetMeta("[ENV-VAR]", env.Name+"="+env.Value)
}
}
c.SetMeta("IPs", insp.Status.PodIP)
c.SetMeta("created", insp.CreationTimestamp.Format("Mon Jan 2 15:04:05 2006"))
c.SetMeta("health", string(insp.Status.Phase))
c.SetState("running")
}
func k8sPort(ports []v1.ContainerPort) string {
str := []string{}
for _, p := range ports {
str = append(str, fmt.Sprintf("%s:%d -> %d", p.HostIP, p.HostPort, p.ContainerPort))
}
return strings.Join(str, "\n")
}
func (k *Kubernetes) inspect(id string) *v1.Pod {
p, err := k.clientset.CoreV1().Pods(k.namespace).Get(id, metav1.GetOptions{})
if err != nil {
if _, ok := err.(*api.NoSuchContainer); !ok {
log.Errorf(err.Error())
}
}
return p
}
// Remove containers by ID
func (k *Kubernetes) delByID(name string) {
k.lock.Lock()
delete(k.containers, name)
k.lock.Unlock()
log.Infof("removed dead container: %s", name)
}
func (k *Kubernetes) Get(name string) (c *container.Container, ok bool) {
k.lock.Lock()
c, ok = k.containers[name]
k.lock.Unlock()
return
}
// Mark all container IDs for refresh
func (k *Kubernetes) refreshAll() {
allPods, err := k.clientset.CoreV1().Pods(k.namespace).List(metav1.ListOptions{})
if err != nil {
log.Error(err.Error())
return
}
for _, pod := range allPods.Items {
c := k.MustGet(pod.Name)
c.SetMeta("uid", string(pod.UID))
c.SetMeta("name", pod.Name)
if pod.Initializers != nil && pod.Initializers.Result != nil {
c.SetState(pod.Initializers.Result.Status)
} else {
c.SetState(string(pod.Status.Phase))
}
k.needsRefresh <- c.Id
}
}
func (k *Kubernetes) All() (containers container.Containers) {
k.lock.Lock()
for _, c := range k.containers {
containers = append(containers, c)
}
containers.Sort()
containers.Filter()
k.lock.Unlock()
return containers
}
func homeDir() string {
if h := os.Getenv("HOME"); h != "" {
return h
}
return os.Getenv("USERPROFILE") // windows
}

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -1,64 +0,0 @@
package manager
import (
"k8s.io/client-go/kubernetes"
)
type Kubernetes struct {
id string
client *kubernetes.Clientset
}
func NewKubernetes(client *kubernetes.Clientset, id string) *Kubernetes {
return &Kubernetes{
id: id,
client: client,
}
}
func (dc *Kubernetes) Start() error {
//c, err := dc.client.InspectContainer(dc.id)
//if err != nil {
// return fmt.Errorf("cannot inspect container: %v", err)
//}
//if err := dc.client.StartContainer(c.ID, c.HostConfig); err != nil {
// return fmt.Errorf("cannot start container: %v", err)
//}
return nil
}
func (dc *Kubernetes) Stop() error {
//if err := dc.client.StopContainer(dc.id, 3); err != nil {
// return fmt.Errorf("cannot stop container: %v", err)
//}
return nil
}
func (dc *Kubernetes) Remove() error {
//if err := dc.client.RemoveContainer(api.RemoveContainerOptions{ID: dc.id}); err != nil {
// return fmt.Errorf("cannot remove container: %v", err)
//}
return nil
}
func (dc *Kubernetes) Pause() error {
//if err := dc.client.PauseContainer(dc.id); err != nil {
// return fmt.Errorf("cannot pause container: %v", err)
//}
return nil
}
func (dc *Kubernetes) Unpause() error {
//if err := dc.client.UnpauseContainer(dc.id); err != nil {
// return fmt.Errorf("cannot unpause container: %v", err)
//}
return nil
}
func (dc *Kubernetes) Restart() error {
//if err := dc.client.RestartContainer(dc.id, 3); err != nil {
// return fmt.Errorf("cannot restart container: %v", err)
//}
return nil
}

View File

@@ -7,4 +7,5 @@ type Manager interface {
Pause() error
Unpause() error
Restart() error
Exec(cmd []string) error
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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 {
select {
case <-cm.closed:
return
case <-time.After(5 * time.Second):
cm.refreshAll()
time.Sleep(5 * time.Second)
}
}
}()
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))
}
}

View File

@@ -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)
}

View File

@@ -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 || gc.selectedID == "" {
gc.Reset()
}
if !cursorVisible {
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 {

View File

@@ -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,31 +57,25 @@ 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":
case "healthy":
color = ui.ThemeAttr("status.ok")
case "unhealthy", "Failed", "Unknown":
case "unhealthy":
color = ui.ThemeAttr("status.danger")
case "starting", "Pending", "Running":
case "starting":
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)
}

View File

@@ -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

View File

@@ -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

53
go.mod
View File

@@ -1,61 +1,32 @@
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
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
github.com/docker/go-units v0.3.2 // indirect
github.com/fsouza/go-dockerclient v0.0.0-20170307141636-318513eb1ab2
github.com/ghodss/yaml v1.0.0 // indirect
github.com/cyphar/filepath-securejoin v0.2.2 // indirect
github.com/fsouza/go-dockerclient v1.4.1
github.com/gizak/termui v2.3.0+incompatible
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55 // indirect
github.com/gogo/protobuf v1.1.1 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/googleapis/gnostic v0.2.0 // indirect
github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f // indirect
github.com/hashicorp/go-cleanhttp v0.0.0-20170211013415-3573b8b52aa7 // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c
github.com/json-iterator/go v1.1.5 // indirect
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/mrunalp/fileutils v0.0.0-20171103030105-7d4729fb3618 // indirect
github.com/nsf/termbox-go v0.0.0-20180303152453-e2050e41c884
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473
github.com/opencontainers/runc v0.1.1
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/opencontainers/runc v1.0.0-rc8
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/seccomp/libseccomp-golang v0.0.0-20150813023252-1b506fc7c24e // indirect
github.com/spf13/pflag v1.0.3 // indirect
github.com/stretchr/testify v1.2.2 // indirect
github.com/syndtr/gocapability v0.0.0-20150716010906-2c00daeb6c3b // indirect
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 // indirect
github.com/vishvananda/netlink v0.0.0-20150820014904-1e2e08e8a2dc // indirect
github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc // indirect
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85 // indirect
golang.org/x/oauth2 v0.0.0-20181128211412-28207608b838 // indirect
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 // indirect
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
google.golang.org/appengine v1.3.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.2.2 // indirect
k8s.io/api v0.0.0-20181130031204-d04500c8c3dd
k8s.io/apimachinery v0.0.0-20181201231028-18a5ff3097b4
k8s.io/client-go v9.0.0+incompatible
k8s.io/klog v0.1.0 // indirect
k8s.io/metrics v0.0.0-20181121073115-d8618695b08f
sigs.k8s.io/yaml v1.1.0 // indirect
)
replace github.com/gizak/termui => github.com/bcicen/termui v0.0.0-20180326052246-4eb80249d3f5
go 1.13

103
go.sum Normal file
View File

@@ -0,0 +1,103 @@
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY=
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.4.12 h1:xAfWHN1IrQ0NJ9TBC0KBZoqLjzDTr1ML+4MywiUOryc=
github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/bcicen/termui v0.0.0-20180326052246-4eb80249d3f5 h1:2pI3ZsoefWIi++8EqmANoC7Px/v2lRwnleVUcCuFgLg=
github.com/bcicen/termui v0.0.0-20180326052246-4eb80249d3f5/go.mod h1:yIA9ITWZD1p4/DvCQ44xvhyVb9XEUlVnY1rzGSHwbiM=
github.com/c9s/goprocinfo v0.0.0-20170609001544-b34328d6e0cd h1:xqaBnULC8wEnQpRDXAsDgXkU/STqoluz1REOoegSfNU=
github.com/c9s/goprocinfo v0.0.0-20170609001544-b34328d6e0cd/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE=
github.com/checkpoint-restore/go-criu v0.0.0-20190109184317-bdb7599cd87b h1:T4nWG1TXIxeor8mAu5bFguPJgSIGhZqv/f0z55KCrJM=
github.com/checkpoint-restore/go-criu v0.0.0-20190109184317-bdb7599cd87b/go.mod h1:TrMrLQfeENAPYPRsJuq3jsqdlRh3lvi6trTZJG8+tho=
github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50 h1:WMpHmC6AxwWb9hMqhudkqG7A/p14KiMnl6d3r1iUMjU=
github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 h1:4BX8f882bXEDKfWIf0wa8HRvpnBoPszJJXL+TVbBw4M=
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/coreos/go-systemd v0.0.0-20151104194251-b4a58d95188d h1:MJ4ge3i0lehw+gE3JcGUUp8TmWjsLAlQlhmdASs/9wk=
github.com/coreos/go-systemd v0.0.0-20151104194251-b4a58d95188d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg=
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/docker v0.7.3-0.20190309235953-33c3200e0d16 h1:dmUn0SuGx7unKFwxyeQ/oLUHhEfZosEDrpmYM+6MTuc=
github.com/docker/docker v0.7.3-0.20190309235953-33c3200e0d16/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fsouza/go-dockerclient v1.4.1 h1:W7wuJ3IB48WYZv/UBk9dCTIb9oX805+L9KIm65HcUYs=
github.com/fsouza/go-dockerclient v1.4.1/go.mod h1:PUNHxbowDqRXfRgZqMz1OeGtbWC6VKyZvJ99hDjB0qs=
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55 h1:oIgNYSrSUbNH5DJh6DMhU1PiOKOYIHNxrV3djLsLpEI=
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk=
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/ijc/Gotty v0.0.0-20170406111628-a8b993ba6abd h1:anPrsicrIi2ColgWTVPk+TrN42hJIWlfPHSBP9S0ZkM=
github.com/ijc/Gotty v0.0.0-20170406111628-a8b993ba6abd/go.mod h1:3LVOLeyx9XVvwPgrt2be44XgSqndprz1G18rSk8KD84=
github.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c h1:/hc+TxW4Q1v6aqNPHE5jiaNF2xEK0CzWTgo25RQhQ+U=
github.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c/go.mod h1:FJRkXmPrkHw0WDjB/LXMUhjWJ112Y6JUYnIVBOy8oH8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c h1:eFzthqtg3W6Pihj3DMTXLAF4f+ge5r5Ie5g6HLIZAF0=
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mrunalp/fileutils v0.0.0-20171103030105-7d4729fb3618 h1:7InQ7/zrOh6SlFjaXFubv0xX0HsuC9qJsdqm7bNQpYM=
github.com/mrunalp/fileutils v0.0.0-20171103030105-7d4729fb3618/go.mod h1:x8F1gnqOkIEiO4rqoeEEEqQbo7HjGMTvyoq3gej4iT0=
github.com/nsf/termbox-go v0.0.0-20180303152453-e2050e41c884 h1:fcs71SMqqDhUD+PbpIv9xf3EH9F9s6HfiLwr6jKm1VA=
github.com/nsf/termbox-go v0.0.0-20180303152453-e2050e41c884/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473 h1:J1QZwDXgZ4dJD2s19iqR9+U00OWM2kDzbf1O/fmvCWg=
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runc v1.0.0-rc8 h1:dDCFes8Hj1r/i5qnypONo5jdOme/8HWZC/aNDyhECt0=
github.com/opencontainers/runc v1.0.0-rc8/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runtime-spec v1.0.1 h1:wY4pOY8fBdSIvs9+IDHC55thBuEulhzfSgKeC1yFvzQ=
github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.2.2 h1:Kx9J6eDG5/24A6DtUquGSpJQ+m2MUTahn4FtGEe8bFg=
github.com/opencontainers/selinux v1.2.2/go.mod h1:+BLncwf63G4dgOzykXAxcmnFlUaOlkDdmw/CqsW6pjs=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/seccomp/libseccomp-golang v0.0.0-20150813023252-1b506fc7c24e h1:HJbgNpzYMeTLPpkMwbPNTPlhNd9r4xQtqcZG6qoIGgs=
github.com/seccomp/libseccomp-golang v0.0.0-20150813023252-1b506fc7c24e/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 h1:b6uOv7YOFK0TYG7HtkIgExQo+2RdLuwRft63jn2HWj8=
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/vishvananda/netlink v0.0.0-20150820014904-1e2e08e8a2dc h1:0HAHLwEY4k1VqaO1SzBi4XxT0KA06Cv+QW2LXknBk9g=
github.com/vishvananda/netlink v0.0.0-20150820014904-1e2e08e8a2dc/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc h1:R83G5ikgLMxrBvLh22JhdfI8K6YXEPHx5P03Uu3DRs4=
github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190310054646-10058d7d4faa h1:lqti/xP+yD/6zH5TqEwx2MilNIJY5Vbc6Qr8J3qyPIQ=
golang.org/x/sys v0.0.0-20190310054646-10058d7d4faa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=

66
grid.go
View File

@@ -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 {

41
main.go
View File

@@ -27,26 +27,28 @@ var (
cGrid *compact.CompactGrid
header *widgets.CTopHeader
status *widgets.StatusLine
errView *widgets.ErrorView
versionStr = fmt.Sprintf("ctop version %v, build %v %v", version, build, goVersion)
fs = flag.NewFlagSet("ctop", flag.ExitOnError)
versionFlag = fs.Bool("version", false, "output version information and exit")
helpFlag = fs.Bool("h", false, "display this help dialog")
filterFlag = fs.String("f", "", "filter containers")
activeOnlyFlag = fs.Bool("a", false, "show active containers only")
sortFieldFlag = fs.String("s", "", "select container sort field")
reverseSortFlag = fs.Bool("r", false, "reverse container sort order")
invertFlag = fs.Bool("i", false, "invert default colors")
scaleCpu = fs.Bool("scale-cpu", false, "show cpu as % of system total")
connectorFlag = fs.String("connector", "docker", "container connector to use")
namespaceFlag = fs.String("n", "default", "Kubernetes namespace for monitoring")
)
func main() {
defer panicExit()
fs.Parse(os.Args[1:])
// parse command line arguments
var (
versionFlag = flag.Bool("v", false, "output version information and exit")
helpFlag = flag.Bool("h", false, "display this help dialog")
filterFlag = flag.String("f", "", "filter containers")
activeOnlyFlag = flag.Bool("a", false, "show active containers only")
sortFieldFlag = flag.String("s", "", "select container sort field")
reverseSortFlag = flag.Bool("r", false, "reverse container sort order")
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()
if *versionFlag {
fmt.Println(versionStr)
@@ -87,11 +89,14 @@ func main() {
config.Toggle("scaleCpu")
}
if *defaultShell != "" {
config.Update("shell", *defaultShell)
}
// init ui
if *invertFlag {
InvertColorMap()
}
config.Update("namespace", *namespaceFlag)
ui.ColorMap = ColorMap // override default colormap
if err := ui.Init(); err != nil {
panic(err)
@@ -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)
}
@@ -150,7 +157,7 @@ options:
func printHelp() {
fmt.Println(helpMsg)
fs.PrintDefaults()
flag.PrintDefaults()
fmt.Printf("\navailable connectors: ")
fmt.Println(strings.Join(connector.Enabled(), ", "))
}

109
menus.go
View File

@@ -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,36 +127,97 @@ 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/<enter>", func(ui.Event) {
switch m.SelectedItem().Val {
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":
@@ -169,12 +231,7 @@ func ContainerMenu() MenuFn {
case "restart":
nextMenu = Confirm(confirmTxt("restart", c.GetMeta("name")), c.Restart)
}
ui.StopLoop()
})
ui.Handle("/sys/kbd/", func(ui.Event) {
ui.StopLoop()
})
ui.Loop()
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 {

60
widgets/error.go Normal file
View File

@@ -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))
}

View File

@@ -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(),

View File

@@ -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 {