Skip to content

Commit

Permalink
feat: monitor command
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcel Kloubert committed May 26, 2024
1 parent a429ce2 commit c1a5728
Show file tree
Hide file tree
Showing 9 changed files with 362 additions and 12 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 0.15.0

- feat: `monitor` command, which displays CPU and memory usage of a process in the console
- refactor: using new and shorter `CheckForError()` function instead of `CloseWithError()` in most cases
- refactor: improve logging

Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
- [List aliases](#list-aliases-)
- [List executables](#list-executables-)
- [List projects](#list-projects-)
- [Monitor process](#monitor-process-)
- [New project](#new-project-)
- [Open alias](#open-alias-)
- [Open project](#open-project-)
Expand Down Expand Up @@ -341,6 +342,10 @@ mkloubert
[email protected]:mkloubert/mkloubert.git
```

#### Monitor process [<a href="#commands-">↑</a>]

![Monitor Demo 1](./img/demos/monitor-demo-1.gif)

#### New project [<a href="#commands-">↑</a>]

`gpm new <project>` is designed to setup a project via an alias defined with [Add project](#add-project-) command.
Expand Down Expand Up @@ -644,5 +649,7 @@ The project is licensed under the [MIT](./LICENSE).
- [go-version](https://github.com/hashicorp/go-version) by [HashiCorp](https://github.com/hashicorp)
- [go-yaml](https://github.com/goccy/go-yaml) by [Masaaki Goshima](https://github.com/sponsors/goccy)
- [godotenv](https://github.com/joho/godotenv) by [John Barton](https://github.com/joho)
- [gopsutil](https://github.com/shirou/gopsutil) by [shirou](https://github.com/sponsors/shirou)
- [progressbar](https://github.com/schollz/progressbar) by [Zack](https://github.com/sponsors/schollz)
- [spinner](https://github.com/sponsors/briandowns) by [Brian Downs](https://github.com/sponsors/briandowns)
- [termui](https://github.com/gizak/termui) by [Zack Guo](https://github.com/gizak)
4 changes: 4 additions & 0 deletions aliases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ aliases:
- https://github.com/schollz/progressbar/v3
prompt:
- https://github.com/c-bata/go-prompt
psutil:
- https://github.com/shirou/gopsutil/v3
spinner:
- https://github.com/briandowns/spinner
termui:
- https://github.com/gizak/termui/v3
tview:
- https://github.com/rivo/tview
version:
Expand Down
248 changes: 248 additions & 0 deletions commands/monitor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
// MIT License
//
// Copyright (c) 2024 Marcel Joachim Kloubert (https://marcel.coffee)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

package commands

import (
"fmt"
"log"
"strconv"
"strings"
"time"

"github.com/spf13/cobra"

"github.com/shirou/gopsutil/v3/mem"
"github.com/shirou/gopsutil/v3/process"

ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"

"github.com/mkloubert/go-package-manager/types"
"github.com/mkloubert/go-package-manager/utils"
)

func Init_Monitor_Command(parentCmd *cobra.Command, app *types.AppContext) {
var cpuDataSize int
var cpuZoom float64
var interval int
var memDataSize int
var memZoom float64

var monitorCmd = &cobra.Command{
Use: "monitor [pid or name]",
Aliases: []string{"mon"},
Args: cobra.MinimumNArgs(1),
Short: "Monitor process",
Long: `Monitors a process by PID or its name.`,
Run: func(cmd *cobra.Command, args []string) {
processes, err := process.Processes()
utils.CheckForError(err)

pidOrName := strings.TrimSpace(
strings.ToLower(args[0]),
)

var processToMonitor *process.Process

i64Val, err := strconv.ParseInt(pidOrName, 10, 32)
if err == nil {
// valid number, search by name
pid := int32(i64Val)

for _, p := range processes {
if p.Pid == pid {
processToMonitor = p
break
}
}
} else {
matchingProcesses := []*process.Process{}

for _, p := range processes {
name, err := p.Name()
if err != nil {
continue
}

lowerName := strings.TrimSpace(
strings.ToLower(name),
)

shouldAddProcess := true

for _, part := range args {
lowerPart := strings.ToLower(part)
if !strings.Contains(lowerName, lowerPart) {
shouldAddProcess = false
break
}
}

if shouldAddProcess {
matchingProcesses = append(matchingProcesses, p)
}
}

matchingProcessesCount := len(matchingProcesses)
if matchingProcessesCount == 1 {
processToMonitor = matchingProcesses[0]
} else if matchingProcessesCount > 1 {
utils.CloseWithError(fmt.Errorf("found %v matching process", matchingProcessesCount))
}
}

if processToMonitor == nil {
utils.CloseWithError(fmt.Errorf("process %v not found", pidOrName))
}

if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
defer ui.Close()

// data
cpuData := []float64{0}
memData := []float64{0}

// CPU diagram
slCpu := widgets.NewSparkline()
slCpu.Data = cpuData
slCpu.MaxVal = 100 / cpuZoom

// memory diagram
slMem := widgets.NewSparkline()
slMem.Data = memData

vMem, err := mem.VirtualMemory()
if err == nil {
slMem.MaxVal = float64(vMem.Total) / memZoom
}

rerender := func() {
currentCpu := cpuData[0]
currentMem := memData[0]

processName, _ := processToMonitor.Name()
processPid := processToMonitor.Pid

termWidth, termHeight := ui.TerminalDimensions()

grid := ui.NewGrid()
grid.SetRect(0, 3, termWidth, termHeight)

pTitle := widgets.NewParagraph()
pTitle.Text = fmt.Sprintf("%v (%v)", processName, processPid)
pTitle.SetRect(0, 0, termWidth, 3)
pTitle.Border = true

// create a sparkline group from CPU widgets
slgCpu := widgets.NewSparklineGroup(slCpu)
slgCpu.Title = fmt.Sprintf(
"CPU %.2f%% (%.1fx)",
currentCpu, cpuZoom,
)
slgCpu.SetRect(0, 0, termWidth, termHeight-3)

// create a sparkline group from memory widgets
slgMem := widgets.NewSparklineGroup(slMem)
slgMem.Title = fmt.Sprintf(
"MEM %vMB / %vMB (%.1fx)",
fmt.Sprintf("%.2f", currentMem/1024.0/1024.0),
fmt.Sprintf("%.2f", float64(vMem.Total)/1024.0/1024.0),
memZoom,
)
slgMem.SetRect(0, 0, termWidth, termHeight-3)

// create grid ...
grid.Set(
// with 1 row
ui.NewRow(1,
// and 2 columns
// with 50%/50% size
ui.NewCol(1.0/2, slgCpu),
ui.NewCol(1.0/2, slgMem),
),
)

// render whole UI
ui.Render(pTitle, grid)
}

shouldRun := true
go func() {
for shouldRun {
// wait before continue
time.Sleep(time.Duration(interval) * time.Millisecond)

// memory usage
memInfo, err := processToMonitor.MemoryInfo()
if err == nil {
memData = append([]float64{float64(memInfo.RSS)}, memData...)
} else {
memData = append([]float64{-1}, memData...)
}
memData = utils.EnsureMaxSliceLength(memData, memDataSize)

// CPU usage
cpuPercent, err := processToMonitor.CPUPercent()
if err == nil {
cpuData = append([]float64{cpuPercent}, cpuData...)
} else {
cpuData = append([]float64{-1}, cpuData...)
}
cpuData = utils.EnsureMaxSliceLength(cpuData, cpuDataSize)

// update data ...
utils.UpdateUsageSparkline(slMem, memData)
utils.UpdateUsageSparkline(slCpu, cpuData)

// .. before rerender
rerender()
}
}()

rerender() // initial rendering

uiEvents := ui.PollEvents()
for {
e := <-uiEvents
switch e.ID {
case "q", "<C-c>":
// CTRL + C
shouldRun = false
return
}
}
},
}

monitorCmd.Flags().IntVarP(&cpuDataSize, "cpu-data-size", "", 360, "custom size of maximum data items for CPU sparkline")
monitorCmd.Flags().Float64VarP(&cpuZoom, "cpu-zoom", "", 1.0, "zoom factor for CPU sparkline")
monitorCmd.Flags().IntVarP(&interval, "interval", "", 500, "time in milliseconds for the update interval")
monitorCmd.Flags().IntVarP(&memDataSize, "mem-data-size", "", 360, "custom size of maximum data items for mem sparkline")
monitorCmd.Flags().Float64VarP(&memZoom, "mem-zoom", "", 1.0, "zoom factor for mem sparkline")

parentCmd.AddCommand(
monitorCmd,
)
}
11 changes: 11 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,36 @@ require (
github.com/briandowns/spinner v1.23.0
github.com/c-bata/go-prompt v0.2.6
github.com/fatih/color v1.17.0
github.com/gizak/termui/v3 v3.1.0
github.com/goccy/go-yaml v1.11.3
github.com/hashicorp/go-version v1.6.0
github.com/jedib0t/go-pretty/v6 v6.5.9
github.com/joho/godotenv v1.5.1
github.com/schollz/progressbar/v3 v3.14.2
github.com/shirou/gopsutil/v3 v3.24.4
github.com/spf13/cobra v1.8.0
)

require (
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mattn/go-tty v0.0.5 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/nsf/termbox-go v1.1.1 // indirect
github.com/pkg/term v1.2.0-beta.2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.20.0 // indirect
Expand Down
Loading

0 comments on commit c1a5728

Please sign in to comment.