osctl top enhancements (#568)

* feat(osctl): Automatic sizing of top window

Signed-off-by: Brad Beam <brad.beam@talos-systems.com>

* feat(osctl): Format top output in proper columns

Signed-off-by: Brad Beam <brad.beam@talos-systems.com>

* feat(osctl): Add sort by cpu/rss options

Signed-off-by: Brad Beam <brad.beam@talos-systems.com>

* feat(osctl): Add ability to run once (no gui)

Signed-off-by: Brad Beam <brad.beam@talos-systems.com>
This commit is contained in:
Brad Beam 2019-04-24 16:44:57 -05:00 committed by GitHub
parent 68c2a2735d
commit 1a5be8da47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 81 additions and 19 deletions

View File

@ -10,13 +10,16 @@ import (
"sort"
"time"
"code.cloudfoundry.org/bytefmt"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
"github.com/ryanuber/columnize"
"github.com/spf13/cobra"
"github.com/talos-systems/talos/cmd/osctl/pkg/client"
"github.com/talos-systems/talos/cmd/osctl/pkg/helpers"
"github.com/talos-systems/talos/internal/pkg/constants"
"github.com/talos-systems/talos/internal/pkg/proc"
"golang.org/x/crypto/ssh/terminal"
)
// versionCmd represents the version command
@ -34,6 +37,18 @@ var topCmd = &cobra.Command{
helpers.Fatalf("error constructing client: %s", err)
}
if oneTime {
var output string
output, err = topOutput(c)
if err != nil {
log.Fatal(err)
}
// Note this is unlimited output of process lines
// we arent artificially limited by the box we would otherwise draw
fmt.Println(output)
return
}
if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
@ -43,42 +58,43 @@ var topCmd = &cobra.Command{
},
}
var sortMethod string
var oneTime bool
func init() {
topCmd.Flags().StringVarP(&sortMethod, "sort", "s", "rss", "Column to sort output by. [rss|cpu]")
topCmd.Flags().BoolVarP(&oneTime, "once", "1", false, "Print the current top output ( no gui/auto refresh )")
rootCmd.AddCommand(topCmd)
}
func topUI(c *client.Client) {
l := widgets.NewList()
l := widgets.NewParagraph()
l.Title = "Top"
// TODO see if we can dynamically get this
// x, y, w, h
l.SetRect(0, 0, 65, 30)
l.TextStyle.Fg = ui.ColorYellow
draw := func(procs []proc.ProcessList) {
rss := func(p1, p2 *proc.ProcessList) bool {
// Reverse sort ( Descending )
return p1.ResidentMemory > p2.ResidentMemory
draw := func(output string) {
l.Text = output
// Attempt to get terminal dimensions
// Since we're getting this data on each call
// we'll be able to handle terminal window resizing
w, h, err := terminal.GetSize(0)
if err != nil {
log.Fatal("Unable to determine terminal size")
}
by(rss).sort(procs)
s := make([]string, 0, len(procs))
s = append(s, fmt.Sprintf("%s %s %s %s %s %s %s %s", "PID", "State", "Threads", "CPU Time", "VirtMem", "ResMem", "Command", "Exec/Args"))
for _, p := range procs {
//log.Printf("Sorted RSS: %+v", p)
s = append(s, p.String())
}
l.Rows = s
// x, y, w, h
l.SetRect(0, 0, w, h)
ui.Render(l)
}
procs, err := c.Top()
procs, err := topOutput(c)
if err != nil {
log.Println(err)
return
}
draw(procs)
uiEvents := ui.PollEvents()
ticker := time.NewTicker(time.Second).C
for {
@ -87,9 +103,13 @@ func topUI(c *client.Client) {
switch e.ID {
case "q", "<C-c>":
return
case "r", "m":
sortMethod = "rss"
case "c":
sortMethod = "cpu"
}
case <-ticker:
procs, err := c.Top()
procs, err := topOutput(c)
if err != nil {
log.Println(err)
return
@ -128,3 +148,41 @@ func (s *procSorter) Swap(i, j int) {
func (s *procSorter) Less(i, j int) bool {
return s.by(&s.procs[i], &s.procs[j])
}
// Sort Methods
var rss = func(p1, p2 *proc.ProcessList) bool {
// Reverse sort ( Descending )
return p1.ResidentMemory > p2.ResidentMemory
}
var cpu = func(p1, p2 *proc.ProcessList) bool {
// Reverse sort ( Descending )
return p1.CPUTime > p2.CPUTime
}
func topOutput(c *client.Client) (output string, err error) {
procs, err := c.Top()
if err != nil {
log.Println(err)
return
}
switch sortMethod {
case "cpu":
by(cpu).sort(procs)
default:
by(rss).sort(procs)
}
s := make([]string, 0, len(procs))
s = append(s, "PID | State | Threads | CPU Time | VirtMem | ResMem | Command | Exec/Args")
for _, p := range procs {
s = append(s,
fmt.Sprintf("%6d | %1s | %4d | %8.2f | %7s | %7s | %s | %s",
p.Pid, p.State, p.NumThreads, p.CPUTime, bytefmt.ByteSize(p.VirtualMemory), bytefmt.ByteSize(p.ResidentMemory), p.Command, p.Executable))
}
output = columnize.SimpleFormat(s)
return
}

2
go.mod
View File

@ -50,6 +50,7 @@ require (
github.com/pkg/errors v0.8.1
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/procfs v0.0.0-20190416084830-8368d24ba045
github.com/ryanuber/columnize v2.1.0+incompatible
github.com/sirupsen/logrus v1.0.6 // indirect
github.com/spf13/afero v1.2.0 // indirect
github.com/spf13/cobra v0.0.3
@ -62,6 +63,7 @@ require (
github.com/vishvananda/netlink v1.0.0
github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc // indirect
github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
golang.org/x/net v0.0.0-20190313220215-9f648a60d977
golang.org/x/sys v0.0.0-20190312061237-fead79001313
golang.org/x/text v0.3.0

2
go.sum
View File

@ -118,6 +118,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/procfs v0.0.0-20190416084830-8368d24ba045 h1:Raos9GP+3BlCBicScEQ+SjTLpYYac34fZMoeqj9McSM=
github.com/prometheus/procfs v0.0.0-20190416084830-8368d24ba045/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/ryanuber/columnize v2.1.0+incompatible h1:j1Wcmh8OrK4Q7GXY+V7SVSY8nUWQxHW5TkBe7YUl+2s=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sirupsen/logrus v1.0.6 h1:hcP1GmhGigz/O7h1WVUM5KklBp1JoNS9FggWKdj/j3s=
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/spf13/afero v1.2.0 h1:O9FblXGxoTc51M+cqr74Bm2Tmt4PvkA5iu/j8HrkNuY=