Commit b670ea3c authored by liming6's avatar liming6
Browse files

feature 添加进程树视图

parent 70a649e8
......@@ -277,6 +277,7 @@ type DCUProcessInfo struct {
}
type ProcessInfo struct {
Ppid int32 // 父进程id
Pid int32 // 进程号
User string // 用户名或uid
CPU float64 // CPU使用率
......@@ -304,7 +305,7 @@ func getProcessInfo(pids []int32) map[int32]ProcessInfo {
item.Mem, _ = p.MemoryPercent()
t, err := p.Times()
if err == nil {
item.Time = durationStr(time.Duration((t.System + t.User)) * time.Second)
item.Time = DurationStr(time.Duration(t.System+t.User) * time.Second)
}
item.Cmd, _ = p.Cmdline()
d, have := dockerInfo[item.Pid]
......@@ -400,10 +401,14 @@ func (m *DCUInfoMap) GetDCUProcessInfo2() map[int][]DCUProcessInfo {
return result
}
// durationStr 将时间段格式化为 小时:分钟:秒s的格式
func durationStr(d time.Duration) string {
// DurationStr 将时间段格式化为 小时:分钟:秒 的格式
func DurationStr(d time.Duration) string {
h := int(math.Floor(d.Hours()))
m := int(d.Minutes()) % 60
s := int(math.Floor(d.Seconds())) % 60
return strings.Replace(fmt.Sprintf("%d:%2d:%2d", h, m, s), " ", "0", -1)
if h <= 96 {
return strings.Replace(fmt.Sprintf("%d:%2d:%2d", h, m, s), " ", "0", -1)
} else {
return fmt.Sprintf("%.1f days", d.Hours()/24)
}
}
......@@ -72,7 +72,7 @@ func (m *ModelDCUInfo) Update(inputMsg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
case *ActionMsg:
m.actionMsg = msg
if msg.Action != nil {
if msg.Action != nil && msg.VM == VMMain {
m.darkModule = true
} else {
m.darkModule = false
......
package tui
import (
"fmt"
"slices"
"strconv"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type Dialog struct {
action ProcessAction
pids map[int32]bool
pointPid *int32
}
var (
BottonStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
BottonActStyle = lipgloss.NewStyle().Border(lipgloss.DoubleBorder()).Foreground(lipgloss.Color("#fd3535ff")).Underline(true)
)
func (d *Dialog) SetAction(ac ProcessAction) {
d.action = ac
}
func NewDialog(pids map[int32]bool) *Dialog {
return &Dialog{
action: PANone,
pids: pids,
pointPid: nil,
}
}
func (d *Dialog) Init() tea.Cmd {
return nil
}
func (d *Dialog) Update(imsg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := imsg.(type) {
case *ActionMsg:
d.action = *msg.Action
d.pids = msg.SelectPids
d.pointPid = msg.PointPid
return d, nil
}
return d, nil
}
func (d *Dialog) View() string {
str := "Send a signal to the following process?\n"
width := lipgloss.Width(str)
sty := lipgloss.NewStyle().Width(width).Align(lipgloss.Left)
sb := strings.Builder{}
sb.WriteString(str)
if d.pids != nil {
pids := make([]int, 0, len(d.pids))
for pid, have := range d.pids {
if have {
pids = append(pids, int(pid))
}
}
slices.Sort(pids)
for _, pid := range pids {
strPid := strconv.Itoa(int(pid))
sb.WriteString(sty.Render(" · "+strPid) + "\n")
}
} else if d.pointPid != nil {
strPid := strconv.Itoa(int(*d.pointPid))
sb.WriteString(sty.Render(" · "+strPid) + "\n")
}
killButton := BottonStyle.Render(PAKill.String())
termButton := BottonStyle.Render(PATerm.String())
intButton := BottonStyle.Render(PAInt.String())
cancelButton := BottonStyle.Render(PANone.String())
switch d.action {
case PAInt:
intButton = BottonActStyle.Render(PAInt.String())
case PATerm:
termButton = BottonActStyle.Render(PATerm.String())
case PAKill:
killButton = BottonActStyle.Render(PAKill.String())
default:
cancelButton = BottonActStyle.Render(PANone.String())
}
buttons := lipgloss.JoinHorizontal(lipgloss.Top, killButton, termButton, intButton, cancelButton)
buttons = sty.Align(lipgloss.Center).Render(buttons)
sb.WriteString(buttons)
str = lipgloss.NewStyle().Width(width+4).Align(lipgloss.Center).Border(lipgloss.DoubleBorder()).Padding(0, 2).Render(sb.String())
return fmt.Sprintf("%s\n", str)
}
......@@ -49,7 +49,7 @@ func (mh *ModelHeader) Update(inputMsg tea.Msg) (tea.Model, tea.Cmd) {
}
return mh, nil
case *ActionMsg:
if msg.Action != nil {
if msg.Action != nil && msg.VM == VMMain {
mh.darkMode = true
} else {
mh.darkMode = false
......
......@@ -4,6 +4,7 @@ import (
"get-container/cmd/hytop/backend"
"get-container/gpu"
"get-container/utils"
"slices"
"time"
tea "github.com/charmbracelet/bubbletea"
......@@ -21,6 +22,22 @@ var (
LowLeightStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#442d2d"))
NormalStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#e6e6e6eb"))
myBorder = lipgloss.Border{
Top: "═",
TopLeft: "╒",
TopRight: "╕",
Bottom: "═",
BottomLeft: "╘",
BottomRight: "╛",
Left: "│",
Right: "│",
MiddleLeft: "├",
MiddleRight: "┤",
Middle: "┼",
MiddleTop: "┬",
MiddleBottom: "┴",
}
)
// ModelMsg 模型信息,在父组件和各个子组件间共享信息
......@@ -35,22 +52,42 @@ type ModelMsg struct {
type TickMsg time.Time
type ProcessAction int
type ViewMode int
const (
PAKill ProcessAction = 9
PAInt ProcessAction = 2
PATerm ProcessAction = 15
PANone ProcessAction = 0
VMMain ViewMode = 0
VMTree ViewMode = 1
VMOneProcess ViewMode = 2
VMProcessEnv ViewMode = 3
)
func (pa ProcessAction) String() string {
switch pa {
case PAKill:
return "SIGKILL"
case PAInt:
return "SIGINT"
case PATerm:
return "SIGTERM"
default:
return "Cancel"
}
}
// ActionMsg 动作消息
type ActionMsg struct {
VM ViewMode // 所在视图,默认为0,即主视图
SelectPids map[int32]bool // 选择的pid进程
Action *ProcessAction // 对选择的pid的动作
PidView *int32 // 进程视图指标的pid号,为null表示没有进入进程指标视图
PointPid *int32 // 指针指向的pid,为null表示没有选择进程
PointPid *int32 // 指针指向的pid,为null表示没有选择进程,在主视图和进程树视图起作用
PidEnvView *int32 // 进程环境变量视图的pid号,为null表示不进入进程环境变量视图
PidTreeView *int32 // 进入pstree视图的pid号,为null表示不进入pstree视图
PidTreePids []int32 // pid tree视图下,进程id的顺序列表
TargetDCUIndex *int // PointPid使用的dcu index
}
......@@ -63,6 +100,8 @@ type ModelMain struct {
ProcessInfo *ModelProcessInfo
modelMsg *ModelMsg
actionMsg *ActionMsg
dialog *Dialog // 对话框
pstree *ModelPsTree // 进程树视图
}
func NewModelMain(width, height int) ModelMain {
......@@ -73,6 +112,8 @@ func NewModelMain(width, height int) ModelMain {
result.DCUInfo = NewModelDCUInfo(width, height)
result.SysLoad = NewModelSysLoad(width)
result.ProcessInfo = NewModelProcessInfo(width)
result.dialog = NewDialog(nil)
result.pstree = NewModelPsTree(width, height)
result.actionMsg = &ActionMsg{}
return result
}
......@@ -108,6 +149,12 @@ func (m *ModelMain) Init() tea.Cmd {
if c := m.ProcessInfo.Init(); c != nil {
cmds = append(cmds, c)
}
if c := m.dialog.Init(); c != nil {
cmds = append(cmds, c)
}
if c := m.pstree.Init(); c != nil {
cmds = append(cmds, c)
}
m.modelMsg = &modelMsg
// 将调用初始化方法
cmds = append(cmds, tickCmd(), sendMsgCmd(&modelMsg))
......@@ -130,6 +177,9 @@ func (m *ModelMain) Update(inputMsg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Quit
case "h":
return m, tea.Quit
case "left", "right":
cmd := m.handleKeyLR(msg.String())
return m, cmd
case "k":
cmd := m.handleKeyK()
return m, cmd
......@@ -139,6 +189,9 @@ func (m *ModelMain) Update(inputMsg tea.Msg) (tea.Model, tea.Cmd) {
case " ":
cmd := m.handleKeySpace()
return m, cmd
case "t":
cmd := m.handleKeyT()
return m, cmd
}
case TickMsg: // 定时事件
updateModelInfo(m.modelMsg, time.Time(msg))
......@@ -146,10 +199,12 @@ func (m *ModelMain) Update(inputMsg tea.Msg) (tea.Model, tea.Cmd) {
dcuInfo, _ := m.DCUInfo.Update(m.modelMsg)
sysLoad, _ := m.SysLoad.Update(m.modelMsg)
pidinfo, _ := m.ProcessInfo.Update(m.modelMsg)
pstree, _ := m.pstree.Update(m.modelMsg)
m.Header = header.(*ModelHeader)
m.DCUInfo = dcuInfo.(*ModelDCUInfo)
m.SysLoad = sysLoad.(*ModelSysLoad)
m.ProcessInfo = pidinfo.(*ModelProcessInfo)
m.pstree = pstree.(*ModelPsTree)
return m, tickCmd()
case ModelMsg: // 初始化
header, _ := m.Header.Update(m.modelMsg)
......@@ -176,34 +231,78 @@ func (m *ModelMain) Update(inputMsg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m *ModelMain) handleKeyK() tea.Cmd {
if m.actionMsg == nil {
func (m *ModelMain) handleKeyT() tea.Cmd {
// 在主视图下,没有选择任何进程时,可以进入进程树视图,且
if m.actionMsg.VM == VMMain && len(m.actionMsg.SelectPids) == 0 {
m.actionMsg.VM = VMTree
pstree, _ := m.pstree.Update(m.actionMsg)
m.pstree = pstree.(*ModelPsTree)
return nil
} else if m.actionMsg.VM == VMTree {
if len(m.actionMsg.SelectPids) != 0 || m.actionMsg.PointPid != nil {
action := PATerm
m.actionMsg.Action = &action
pstree, _ := m.pstree.Update(m.actionMsg)
dialog, _ := m.dialog.Update(m.actionMsg)
m.dialog = dialog.(*Dialog)
m.pstree = pstree.(*ModelPsTree)
return nil
} else {
return nil
}
}
if m.actionMsg.PointPid == nil && m.actionMsg.SelectPids == nil {
return nil
}
// handleKeyLR 处理左右键消息,左右键仅在对话框中有效
func (m *ModelMain) handleKeyLR(s string) tea.Cmd {
if m.actionMsg == nil || m.actionMsg.Action == nil {
return nil
}
var pa ProcessAction = PANone
if m.actionMsg.Action == nil {
m.actionMsg.Action = &pa
switch s {
case "right":
switch *m.actionMsg.Action {
case PAKill:
*m.actionMsg.Action = PATerm
case PATerm:
*m.actionMsg.Action = PAInt
case PAInt:
*m.actionMsg.Action = PANone
default:
return nil
}
case "left":
switch *m.actionMsg.Action {
case PATerm:
*m.actionMsg.Action = PAKill
case PAInt:
*m.actionMsg.Action = PATerm
case PANone:
*m.actionMsg.Action = PAInt
default:
return nil
}
default:
return nil
}
header, _ := m.Header.Update(m.actionMsg)
dcuInfo, _ := m.DCUInfo.Update(m.actionMsg)
sysLoad, _ := m.SysLoad.Update(m.actionMsg)
pidinfo, _ := m.ProcessInfo.Update(m.actionMsg)
m.Header = header.(*ModelHeader)
m.DCUInfo = dcuInfo.(*ModelDCUInfo)
m.SysLoad = sysLoad.(*ModelSysLoad)
m.ProcessInfo = pidinfo.(*ModelProcessInfo)
dialog, _ := m.dialog.Update(m.actionMsg)
m.dialog = dialog.(*Dialog)
return nil
}
func (m *ModelMain) handleKeyEsc() tea.Cmd {
if m.actionMsg == nil {
func (m *ModelMain) handleKeyK() tea.Cmd {
if m.actionMsg.PointPid == nil && m.actionMsg.SelectPids == nil {
return nil
}
if m.actionMsg.Action != nil {
m.actionMsg.Action = nil
return nil
}
var pa ProcessAction = PANone
if m.actionMsg.Action == nil {
m.actionMsg.Action = &pa
}
switch m.actionMsg.VM {
case VMMain:
header, _ := m.Header.Update(m.actionMsg)
dcuInfo, _ := m.DCUInfo.Update(m.actionMsg)
sysLoad, _ := m.SysLoad.Update(m.actionMsg)
......@@ -212,12 +311,34 @@ func (m *ModelMain) handleKeyEsc() tea.Cmd {
m.DCUInfo = dcuInfo.(*ModelDCUInfo)
m.SysLoad = sysLoad.(*ModelSysLoad)
m.ProcessInfo = pidinfo.(*ModelProcessInfo)
dialog, _ := m.dialog.Update(m.actionMsg)
m.dialog = dialog.(*Dialog)
return nil
case VMTree:
pstree, _ := m.pstree.Update(m.actionMsg)
dialog, _ := m.dialog.Update(m.actionMsg)
m.dialog = dialog.(*Dialog)
m.pstree = pstree.(*ModelPsTree)
return nil
default:
return nil
}
if m.actionMsg.PointPid != nil || m.actionMsg.SelectPids != nil || m.actionMsg.TargetDCUIndex != nil {
m.actionMsg.PointPid = nil
m.actionMsg.SelectPids = nil
m.actionMsg.TargetDCUIndex = nil
}
// handleKeyEsc 处理Esc键,esc键在:
// VMMain视图下,取消所有选择的进程、退出单个进程模式或对话框返回
// VMTree视图下,取消所有选择的进程、退出单个进程模式或对话框返回,返回到VMMain视图
func (m *ModelMain) handleKeyEsc() tea.Cmd {
switch m.actionMsg.VM {
case VMMain:
if m.actionMsg.Action != nil {
m.actionMsg.Action = nil
} else if m.actionMsg.SelectPids != nil {
m.actionMsg.SelectPids = nil
} else if m.actionMsg.PointPid != nil || m.actionMsg.TargetDCUIndex != nil {
m.actionMsg.PointPid = nil
m.actionMsg.TargetDCUIndex = nil
}
header, _ := m.Header.Update(m.actionMsg)
dcuInfo, _ := m.DCUInfo.Update(m.actionMsg)
sysLoad, _ := m.SysLoad.Update(m.actionMsg)
......@@ -226,12 +347,34 @@ func (m *ModelMain) handleKeyEsc() tea.Cmd {
m.DCUInfo = dcuInfo.(*ModelDCUInfo)
m.SysLoad = sysLoad.(*ModelSysLoad)
m.ProcessInfo = pidinfo.(*ModelProcessInfo)
return nil
case VMTree:
if m.actionMsg.Action != nil {
m.actionMsg.Action = nil
} else if m.actionMsg.SelectPids != nil {
m.actionMsg.SelectPids = nil
} else if m.actionMsg.PointPid != nil {
m.actionMsg.PointPid = nil
} else {
m.actionMsg.VM = VMMain
}
pstree, _ := m.pstree.Update(m.actionMsg)
m.pstree = pstree.(*ModelPsTree)
return nil
default:
return nil
}
return nil
}
// handleKeySpace 处理空格键动作,空格键仅用于选择或取消进程
func (m *ModelMain) handleKeySpace() tea.Cmd {
if m.actionMsg == nil || m.actionMsg.PointPid == nil {
if len(m.modelMsg.DCUPidInfo) == 0 {
return nil
}
if m.actionMsg.PointPid == nil || m.actionMsg.Action != nil {
return nil
}
if m.actionMsg.VM != VMMain && m.actionMsg.VM != VMTree {
return nil
}
if m.actionMsg.SelectPids == nil {
......@@ -243,29 +386,40 @@ func (m *ModelMain) handleKeySpace() tea.Cmd {
} else {
m.actionMsg.SelectPids[*m.actionMsg.PointPid] = true
}
m1, cmd1 := m.ProcessInfo.Update(m.actionMsg)
m.ProcessInfo = m1.(*ModelProcessInfo)
return cmd1
switch m.actionMsg.VM {
case VMMain:
m1, cmd1 := m.ProcessInfo.Update(m.actionMsg)
m.ProcessInfo = m1.(*ModelProcessInfo)
return cmd1
case VMTree:
// todo
m1, cmd1 := m.pstree.Update(m.actionMsg)
m.pstree = m1.(*ModelPsTree)
return cmd1
default:
return nil
}
}
func (m *ModelMain) View() string {
return m.Header.View() + m.DCUInfo.View() + m.SysLoad.View() + m.ProcessInfo.View() + "\n"
}
var myBorder = lipgloss.Border{
Top: "═",
TopLeft: "╒",
TopRight: "╕",
Bottom: "═",
BottomLeft: "╘",
BottomRight: "╛",
Left: "│",
Right: "│",
MiddleLeft: "├",
MiddleRight: "┤",
Middle: "┼",
MiddleTop: "┬",
MiddleBottom: "┴",
switch m.actionMsg.VM {
case VMMain:
if m.actionMsg.Action != nil {
up := m.dialog.View()
down := m.Header.View() + m.DCUInfo.View() + m.SysLoad.View() + m.ProcessInfo.View() + "\n"
return StackPosition(up, down, lipgloss.Center, lipgloss.Center)
}
return m.Header.View() + m.DCUInfo.View() + m.SysLoad.View() + m.ProcessInfo.View() + "\n"
case VMTree:
if m.actionMsg.Action != nil {
up := m.dialog.View()
down := m.pstree.View()
return StackPosition(up, down, lipgloss.Center, lipgloss.Center)
}
return m.pstree.View()
default:
return ""
}
}
func initModelInfo(model *ModelMsg) error {
......@@ -308,109 +462,150 @@ func updateModelInfo(modelMsg *ModelMsg, t time.Time) error {
}
func (m *ModelMain) handleKeyUp() tea.Cmd {
if m.actionMsg == nil {
m.actionMsg = &ActionMsg{}
}
if m.modelMsg.DCUPidInfo == nil {
if len(m.modelMsg.DCUPidInfo) == 0 {
return nil
}
if m.actionMsg.Action != nil {
return nil
}
processes := make([]backend.DCUProcessInfo, 0)
for index := range 64 {
p, have := m.modelMsg.DCUPidInfo[index]
if have && len(p) > 0 {
processes = append(processes, p...)
}
}
processNum := len(processes)
if processNum == 0 {
return nil
}
index := 0
if m.actionMsg.PointPid == nil {
// 获取列表中的最后一个进程的pid
index = processNum - 1
} else {
var targetIndex = -1
for l := processNum - 1; l >= 0; l-- {
if processes[l].Info.Pid == *m.actionMsg.PointPid {
targetIndex = l - 1
break
switch m.actionMsg.VM {
case VMMain:
processes := make([]backend.DCUProcessInfo, 0)
for index := range 64 {
p, have := m.modelMsg.DCUPidInfo[index]
if have && len(p) > 0 {
processes = append(processes, p...)
}
}
if targetIndex == -1 {
processNum := len(processes)
if processNum == 0 {
return nil
}
index := 0
if m.actionMsg.PointPid == nil {
// 获取列表中的最后一个进程的pid
index = processNum - 1
} else {
index = targetIndex
var targetIndex = -1
for l := processNum - 1; l >= 0; l-- {
if processes[l].Info.Pid == *m.actionMsg.PointPid {
targetIndex = l - 1
break
}
}
if targetIndex == -1 {
index = processNum - 1
} else {
index = targetIndex
}
}
pid := processes[index].Info.Pid
m.actionMsg.PointPid = &pid
idx := processes[index].DCU
m.actionMsg.TargetDCUIndex = &idx
m1, cmd1 := m.ProcessInfo.Update(m.actionMsg)
m2, cmd2 := m.DCUInfo.Update(m.actionMsg)
m3, cmd3 := m.SysLoad.Update(m.actionMsg)
m.ProcessInfo = m1.(*ModelProcessInfo)
m.DCUInfo = m2.(*ModelDCUInfo)
m.SysLoad = m3.(*ModelSysLoad)
return tea.Batch(cmd1, cmd2, cmd3)
case VMTree:
if len(m.actionMsg.PidTreePids) == 0 {
return nil
}
if m.actionMsg.PointPid == nil {
pid := m.actionMsg.PidTreePids[len(m.actionMsg.PidTreePids)-1]
m.actionMsg.PointPid = &pid
} else {
idx := slices.Index(m.actionMsg.PidTreePids, *m.actionMsg.PointPid)
if idx == -1 || idx == 0 {
*m.actionMsg.PointPid = m.actionMsg.PidTreePids[len(m.actionMsg.PidTreePids)-1]
} else {
*m.actionMsg.PointPid = m.actionMsg.PidTreePids[idx-1]
}
}
pstree, _ := m.pstree.Update(m.actionMsg)
m.pstree = pstree.(*ModelPsTree)
return nil
default:
return nil
}
pid := processes[index].Info.Pid
m.actionMsg.PointPid = &pid
idx := processes[index].DCU
m.actionMsg.TargetDCUIndex = &idx
m1, cmd1 := m.ProcessInfo.Update(m.actionMsg)
m2, cmd2 := m.DCUInfo.Update(m.actionMsg)
m3, cmd3 := m.SysLoad.Update(m.actionMsg)
m.ProcessInfo = m1.(*ModelProcessInfo)
m.DCUInfo = m2.(*ModelDCUInfo)
m.SysLoad = m3.(*ModelSysLoad)
return tea.Batch(cmd1, cmd2, cmd3)
}
func (m *ModelMain) handleKeyDown() tea.Cmd {
if m.actionMsg == nil {
m.actionMsg = &ActionMsg{}
}
if m.modelMsg.DCUPidInfo == nil {
if len(m.modelMsg.DCUPidInfo) == 0 {
return nil
}
if m.actionMsg.Action != nil {
return nil
}
processes := make([]backend.DCUProcessInfo, 0)
for index := range 64 {
p, have := m.modelMsg.DCUPidInfo[index]
if have && len(p) > 0 {
processes = append(processes, p...)
}
}
processNum := len(processes)
if processNum == 0 {
return nil
}
index := 0
if m.actionMsg.PointPid == nil {
index = 0
} else {
var targetIndex = -1
for l := 0; l < processNum-1; l++ {
if processes[l].Info.Pid == *m.actionMsg.PointPid {
targetIndex = l + 1
if targetIndex >= processNum {
targetIndex = 0
}
break
switch m.actionMsg.VM {
case VMMain:
processes := make([]backend.DCUProcessInfo, 0)
for index := range 64 {
p, have := m.modelMsg.DCUPidInfo[index]
if have && len(p) > 0 {
processes = append(processes, p...)
}
}
if targetIndex == -1 {
processNum := len(processes)
if processNum == 0 {
return nil
}
index := 0
if m.actionMsg.PointPid == nil {
index = 0
} else {
index = targetIndex
var targetIndex = -1
for l := 0; l < processNum-1; l++ {
if processes[l].Info.Pid == *m.actionMsg.PointPid {
targetIndex = l + 1
if targetIndex >= processNum {
targetIndex = 0
}
break
}
}
if targetIndex == -1 {
index = 0
} else {
index = targetIndex
}
}
pid := processes[index].Info.Pid
m.actionMsg.PointPid = &pid
idx := processes[index].DCU
m.actionMsg.TargetDCUIndex = &idx
m1, cmd1 := m.ProcessInfo.Update(m.actionMsg)
m2, cmd2 := m.DCUInfo.Update(m.actionMsg)
m3, cmd3 := m.SysLoad.Update(m.actionMsg)
m.ProcessInfo = m1.(*ModelProcessInfo)
m.DCUInfo = m2.(*ModelDCUInfo)
m.SysLoad = m3.(*ModelSysLoad)
return tea.Batch(cmd1, cmd2, cmd3)
case VMTree:
if len(m.actionMsg.PidTreePids) == 0 {
return nil
}
if m.actionMsg.PointPid == nil {
pid := m.actionMsg.PidTreePids[0]
m.actionMsg.PointPid = &pid
} else {
idx := slices.Index(m.actionMsg.PidTreePids, *m.actionMsg.PointPid)
if idx == -1 || idx == len(m.actionMsg.PidTreePids)-1 {
*m.actionMsg.PointPid = m.actionMsg.PidTreePids[0]
} else {
*m.actionMsg.PointPid = m.actionMsg.PidTreePids[idx+1]
}
}
pstree, _ := m.pstree.Update(m.actionMsg)
m.pstree = pstree.(*ModelPsTree)
return nil
default:
return nil
}
pid := processes[index].Info.Pid
m.actionMsg.PointPid = &pid
idx := processes[index].DCU
m.actionMsg.TargetDCUIndex = &idx
m1, cmd1 := m.ProcessInfo.Update(m.actionMsg)
m2, cmd2 := m.DCUInfo.Update(m.actionMsg)
m3, cmd3 := m.SysLoad.Update(m.actionMsg)
m.ProcessInfo = m1.(*ModelProcessInfo)
m.DCUInfo = m2.(*ModelDCUInfo)
m.SysLoad = m3.(*ModelSysLoad)
return tea.Batch(cmd1, cmd2, cmd3)
}
......@@ -77,7 +77,7 @@ func (m *ModelProcessInfo) Init() tea.Cmd {
func (m *ModelProcessInfo) View() string {
darkMode := false
if m.actionMsg != nil && m.actionMsg.Action != nil {
if m.actionMsg != nil && m.actionMsg.Action != nil && m.actionMsg.VM == VMMain {
darkMode = true
}
haveProcess := false
......
package tui
\ No newline at end of file
package tui
import (
"fmt"
"get-container/cmd/hytop/backend"
"get-container/utils"
"slices"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/tree"
)
const (
ModelPsTreeHeader = " PID USER DEVICE %CPU %MEM TIME COMMAND"
)
var (
ModelPsTreeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#000000")).Background(lipgloss.Color("#00fffb7a"))
ModelPsTreeStyleKey = lipgloss.NewStyle().Foreground(lipgloss.Color("#842cffff")).Background(lipgloss.Color("#00fffb7a")).Italic(true)
ModelPsTreeStyleInfo = lipgloss.NewStyle().Foreground(lipgloss.Color("#e82e2eff")).Background(lipgloss.Color("#00fffb7a"))
ModelPsTreeStyleL = lipgloss.NewStyle().Foreground(lipgloss.Color("#656565b0")).Background(lipgloss.Color("#00fffb7a"))
ModelPsTreeStyleKeyL = lipgloss.NewStyle().Foreground(lipgloss.Color("#6a32b992")).Background(lipgloss.Color("#00fffb7a")).Italic(true)
ModelPsTreeStyleInfoL = lipgloss.NewStyle().Foreground(lipgloss.Color("#e82e2e80")).Background(lipgloss.Color("#00fffb7a"))
ModelPsFullHeightStyle = lipgloss.NewStyle()
)
type ModelPsTree struct {
width, height int
modelMsg *ModelMsg
actionMsg *ActionMsg
header [2]string
mapPidToDCU map[int32]int // key为进程ID,value为dcu index
treeWidth int // 树形图的最大宽度
}
func NewModelPsTree(width, height int) *ModelPsTree {
return &ModelPsTree{
width: width,
height: height,
mapPidToDCU: make(map[int32]int),
}
}
func (m *ModelPsTree) Init() tea.Cmd {
// 生成标题字符串
sb := strings.Builder{}
h1 := ModelPsTreeStyle.SetString(ModelPsTreeHeader).String()
sb.WriteString(ModelPsTreeStyle.SetString("(Press ").String())
sb.WriteString(ModelPsTreeStyleKey.SetString("^C").String())
sb.WriteString(ModelPsTreeStyle.SetString("(").String())
sb.WriteString(ModelPsTreeStyleInfo.SetString("INT").String())
sb.WriteString(ModelPsTreeStyle.SetString(")/").String())
sb.WriteString(ModelPsTreeStyleKey.SetString("T").String())
sb.WriteString(ModelPsTreeStyle.SetString("(").String())
sb.WriteString(ModelPsTreeStyleInfo.SetString("TERM").String())
sb.WriteString(ModelPsTreeStyle.SetString(")/").String())
sb.WriteString(ModelPsTreeStyleKey.SetString("K").String())
sb.WriteString(ModelPsTreeStyle.SetString("(").String())
sb.WriteString(ModelPsTreeStyleInfo.SetString("KILL").String())
sb.WriteString(ModelPsTreeStyle.SetString(") to send signals)").String())
h2 := sb.String()
sb.Reset()
w := m.width - lipgloss.Width(h1) - lipgloss.Width(h2)
h := ModelPsTreeStyle.SetString(strings.Repeat(" ", w)).String()
m.header[0] = h1 + h + h2
h1 = ModelPsTreeStyleL.SetString(ModelPsTreeHeader).String()
sb.WriteString(ModelPsTreeStyleL.SetString("(Press ").String())
sb.WriteString(ModelPsTreeStyleKeyL.SetString("^C").String())
sb.WriteString(ModelPsTreeStyleL.SetString("(").String())
sb.WriteString(ModelPsTreeStyleInfoL.SetString("INT").String())
sb.WriteString(ModelPsTreeStyleL.SetString(")/").String())
sb.WriteString(ModelPsTreeStyleKeyL.SetString("T").String())
sb.WriteString(ModelPsTreeStyleL.SetString("(").String())
sb.WriteString(ModelPsTreeStyleInfoL.SetString("TERM").String())
sb.WriteString(ModelPsTreeStyleL.SetString(")/").String())
sb.WriteString(ModelPsTreeStyleKeyL.SetString("K").String())
sb.WriteString(ModelPsTreeStyleL.SetString("(").String())
sb.WriteString(ModelPsTreeStyleInfoL.SetString("KILL").String())
sb.WriteString(ModelPsTreeStyleL.SetString(") to send signals)").String())
h2 = sb.String()
h = ModelPsTreeStyleL.SetString(strings.Repeat(" ", w)).String()
m.header[1] = h1 + h + h2
m.treeWidth = m.width - lipgloss.Width(ModelPsTreeHeader) + lipgloss.Width("COMMAND")
ModelPsFullHeightStyle = ModelPsFullHeightStyle.Height(m.height).Width(m.width).MaxHeight(m.height).MaxWidth(m.width).Align(lipgloss.Center, lipgloss.Top)
return nil
}
func (m *ModelPsTree) View() string {
pids := make([]int32, 0, len(m.mapPidToDCU))
for k := range m.mapPidToDCU {
pids = append(pids, k)
}
slices.Sort(pids)
pinfo, root := utils.GetPsTree(pids)
if len(pinfo) > 0 && root != nil {
darkMode := false
if m.actionMsg.Action != nil && m.actionMsg.VM == VMTree {
darkMode = true
}
tr := tree.New()
tr.Root(root.Cmd)
pidSort := genTree(tr, root, make([]int32, 0, 128))
treeStr := lipgloss.NewStyle().MaxWidth(m.treeWidth).Render(tr.String())
style := lipgloss.NewStyle().Inline(true)
sb := strings.Builder{}
for _, v := range pidSort {
p, have := pinfo[v]
if !have {
sb.WriteByte('\n')
continue
}
sb.WriteString(FormatStr(fmt.Sprintf("%d", p.Pid), 7, lipgloss.Right))
sb.WriteByte(' ')
sb.WriteString(style.MaxWidth(9).Width(9).Align(lipgloss.Left).Render(p.User))
sb.WriteByte(' ')
if dcu, have := m.mapPidToDCU[p.Pid]; have {
sb.WriteString(style.MaxWidth(6).Width(6).Align(lipgloss.Left).Render(fmt.Sprintf("DCU%d", dcu)))
} else {
sb.WriteString(style.MaxWidth(6).Width(6).Align(lipgloss.Left).Render("Host"))
}
sb.WriteByte(' ')
sb.WriteString(style.MaxWidth(5).Width(5).Align(lipgloss.Left).Render(fmt.Sprintf("%.1f", p.CPU)))
sb.WriteByte(' ')
sb.WriteString(style.MaxWidth(5).Width(5).Align(lipgloss.Left).Render(fmt.Sprintf("%.1f", p.Mem)))
sb.WriteByte(' ')
sb.WriteString(style.MaxWidth(9).Width(9).Align(lipgloss.Left).Render(backend.DurationStr(p.Time)))
sb.WriteByte(' ')
sb.WriteByte('\n')
}
infoStr := strings.Trim(sb.String(), "\n")
if m.actionMsg != nil {
if m.actionMsg.PidTreePids != nil {
m.actionMsg.PidTreePids = m.actionMsg.PidTreePids[:0]
m.actionMsg.PidTreePids = append(m.actionMsg.PidTreePids, pidSort...)
} else {
m.actionMsg.PidTreePids = make([]int32, 0, len(pidSort))
m.actionMsg.PidTreePids = append(m.actionMsg.PidTreePids, pidSort...)
}
}
sl := len(m.actionMsg.SelectPids)
downStr := lipgloss.JoinHorizontal(lipgloss.Top, infoStr, treeStr)
if darkMode {
downStr = LowLeightStyle.Render(downStr)
finalStr := lipgloss.JoinVertical(lipgloss.Left, m.header[1], downStr)
return ModelPsFullHeightStyle.Render(finalStr)
}
lines := strings.Split(downStr, "\n")
for index, pid := range pidSort {
selected, pointed := false, false
if sl > 0 {
a, b := m.actionMsg.SelectPids[pid]
selected = a && b
}
if m.actionMsg.PointPid != nil {
pointed = *m.actionMsg.PointPid == pid
}
if selected && pointed {
lines[index] = HeightSelectedStyle.Render(lines[index])
} else if selected {
lines[index] = SelectedStyle.Render(lines[index])
} else if pointed {
lines[index] = HeightLightStyle.Render(lines[index])
}
}
downStr = strings.Join(lines, "\n")
finalStr := lipgloss.JoinVertical(lipgloss.Left, m.header[0], downStr, strings.Repeat("\n", m.height-1-lipgloss.Height(downStr)))
return ModelPsFullHeightStyle.Render(finalStr)
} else {
return m.header[0] + "\n There is no DCU process running"
}
}
func (m *ModelPsTree) Update(imsg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := imsg.(type) {
case *ModelMsg:
m.modelMsg = msg
clear(m.mapPidToDCU)
for k, v := range msg.DCUPidInfo {
for _, val := range v {
m.mapPidToDCU[val.Info.Pid] = k
}
}
case *ActionMsg:
m.actionMsg = msg
}
return m, nil
}
func genTree(t *tree.Tree, root *utils.ProcessInfo, ctx []int32) []int32 {
ctx = append(ctx, root.Pid)
pids := make([]int32, 0, len(root.Child))
for k := range root.Child {
pids = append(pids, k)
}
slices.Sort(pids)
for _, v := range pids {
c := root.Child[v]
tt := tree.New().Root(c.Cmd)
t.Child(tt)
if len(c.Child) > 0 {
ctx = genTree(tt, c, ctx)
} else {
ctx = append(ctx, c.Pid)
}
}
return ctx
}
......@@ -235,10 +235,9 @@ func (m *ModelSysLoad) handleAction() {
func (m *ModelSysLoad) View() string {
darkMode := false
if m.actionMsg != nil && m.actionMsg.Action != nil {
if m.actionMsg != nil && m.actionMsg.Action != nil && m.actionMsg.VM == VMMain {
darkMode = true
}
memUsed := utils.MemorySize{Num: m.current.MemUsed, Unit: utils.Byte}
load := fmt.Sprintf(" Load Average: %.2f %.2f %.2f\n CPU: %.1f%%", m.current.Load1, m.current.Load5, m.current.Load15, m.current.CPUPercent)
......
......@@ -6,6 +6,7 @@ import (
"os"
"regexp"
"strconv"
"time"
"github.com/shirou/gopsutil/v4/process"
)
......@@ -70,3 +71,162 @@ func GetProcessCPUUsage(pid int32) (float64, error) {
}
return p.CPUPercent()
}
type ProcessInfo struct {
Ppid int32
Pid int32
User string
Uid uint32
CPU float64
Mem float32
Time time.Duration
Cmd string
Child map[int32]*ProcessInfo
}
func NewProcessInfo(p *process.Process) *ProcessInfo {
if p == nil {
return nil
}
result := &ProcessInfo{
Pid: p.Pid,
Child: make(map[int32]*ProcessInfo),
}
uids, err := p.Uids()
if err == nil && len(uids) > 0 {
result.Uid = uids[0]
}
user, err := p.Username()
if err == nil {
result.User = user
}
cpu, err := p.CPUPercent()
if err == nil {
result.CPU = cpu
}
mem, err := p.MemoryPercent()
if err == nil {
result.Mem = mem
}
times, err := p.Times()
if err == nil {
result.Time = (time.Duration((times.System + times.User)) * time.Second)
}
cmd, err := p.Cmdline()
if err == nil {
result.Cmd = cmd
}
ppid, err := p.Ppid()
if err == nil {
result.Ppid = ppid
}
return result
}
func GetPsTree(pids []int32) (map[int32]*ProcessInfo, *ProcessInfo) {
findedProcess := make(map[int32]*ProcessInfo)
for _, pid := range pids {
pp := pid
plist := make([]*ProcessInfo, 0, 16)
// 向上追踪
for {
p, have := findedProcess[pp]
if !have {
pInner, err := process.NewProcess(pp)
if err != nil {
break
}
p = NewProcessInfo(pInner)
findedProcess[pInner.Pid] = p
}
plist = append(plist, p)
if p.Pid == 1 {
break
}
pp = p.Ppid
}
plistLen := len(plist)
if plistLen >= 2 {
for i := 0; i <= plistLen-2; i++ {
p := plist[i+1]
p.Child[plist[i].Pid] = plist[i]
}
}
// addChilds(pidProcess, findedProcess)
}
return findedProcess, findedProcess[1]
}
func GetPsTree2(pids []int32) map[int32]*ProcessInfo {
allProcess, err := process.Processes()
if err != nil {
return nil
}
m := make(map[int32]*process.Process)
for _, v := range allProcess {
m[v.Pid] = v
}
findedProcess := make(map[int32]*ProcessInfo)
for _, pid := range pids {
pp := pid
plist := make([]*ProcessInfo, 0, 16)
for {
p, have := findedProcess[pp]
if !have {
pInner, have := m[pp]
if !have {
break
}
p = NewProcessInfo(pInner)
findedProcess[pInner.Pid] = p
}
plist = append(plist, p)
if p.Pid == 1 {
break
}
pp = p.Ppid
}
plistLen := len(plist)
if plistLen >= 2 {
for i := 0; i <= plistLen-2; i++ {
p := plist[i+1]
p.Child[plist[i].Pid] = plist[i]
}
}
}
return findedProcess
}
func addChilds(p *process.Process, ctx map[int32]*ProcessInfo) map[int32]*ProcessInfo {
if ctx == nil {
ctx = make(map[int32]*ProcessInfo)
}
if p == nil {
return ctx
}
start := time.Now()
childs, err := p.Children()
d := time.Since(start)
log.Printf("children %d ms", d.Milliseconds())
if err != nil {
return ctx
}
info, have := ctx[p.Pid]
if !have {
pp := NewProcessInfo(p)
info = pp
ctx[p.Pid] = pp
}
for _, child := range childs {
c, have := ctx[child.Pid]
if !have {
c = NewProcessInfo(child)
ctx[child.Pid] = c
}
info.Child[child.Pid] = c
addChilds(child, ctx)
}
return ctx
}
package utils
type Tree[K comparable, V any] struct {
root *TreeNode[K, V]
size int
flatten map[K]*TreeNode[K, V]
maxDepth int // 最大深度
}
func NewTree[K comparable, V any]() *Tree[K, V] {
return &Tree[K, V]{
root: nil,
size: 0,
flatten: make(map[K]*TreeNode[K, V]),
}
}
func (t *Tree[K, V]) Size() int {
return t.size
}
// AddChildNode 向树中添加节点,如果parent为nill,表示添加此为根节点
// 只能添加一次根节点
func (t *Tree[K, V]) AddChildNode(id K, data V, parent *TreeNode[K, V]) *TreeNode[K, V] {
n := newTreeNode(id, data, nil)
if parent == nil {
if t.root == nil {
t.root = n
t.root.depth = 0
t.size++
} else {
return nil
}
} else {
t.size++
parent.child[n.id] = n
n.parent = parent
n.depth = parent.depth + 1
t.maxDepth = max(t.maxDepth, n.depth)
}
t.flatten[n.id] = n
return n
}
// DelNode 删除节点,如果节点不是叶子节点,那么会删除其所有子节点
func (t *Tree[K, V]) DelNode(id K) *TreeNode[K, V] {
n, have := t.flatten[id]
if !have {
return nil
}
delete(t.flatten, id)
delete(n.parent.child, id)
n.parent = nil
l := n.flattenToList()
for _, v := range l {
v.depth = 0
delete(t.flatten, v.id)
}
t.size -= len(l)
return n
}
func (t *Tree[K, V]) GetNodeById(id K) *TreeNode[K, V] {
return t.flatten[id]
}
func (t *Tree[K, V]) FlattenToList() []*TreeNode[K, V] {
return t.root.flattenToList()
}
func (t *Tree[K, V]) GetRoot() *TreeNode[K, V] {
return t.root
}
func (t *Tree[K, V]) GetDepth() int {
return t.maxDepth
}
type TreeNode[K comparable, V any] struct {
parent *TreeNode[K, V] // 父节点
child map[K]*TreeNode[K, V] // 子节点
Data V // 数据
id K // 唯一性id
depth int // 深度,为根节点时,depth为0,依次递增
}
func (t *TreeNode[K, V]) GetId() K {
return t.id
}
func (t *TreeNode[K, V]) GetParent() *TreeNode[K, V] {
return t.parent
}
func newTreeNode[K comparable, V any](key K, data V, parent *TreeNode[K, V]) *TreeNode[K, V] {
return &TreeNode[K, V]{
parent: parent,
Data: data,
id: key,
child: make(map[K]*TreeNode[K, V]),
depth: 0,
}
}
// FlattenToList 将树展平为切片
func (t *TreeNode[K, V]) flattenToList() []*TreeNode[K, V] {
result := make([]*TreeNode[K, V], 0, 128)
for _, c := range t.childs() {
result = append(result, c) // 添加这个子节点
result = append(result, c.flattenToList()...) // 添加这个子节点的子节点
}
if t.parent == nil { // parent为nil,添加自己
result = append(result, t)
}
return result
}
// Childs 列出子节点
func (t *TreeNode[K, V]) childs() []*TreeNode[K, V] {
l := len(t.child)
result := make([]*TreeNode[K, V], 0, l)
if l == 0 {
return result
}
for _, v := range t.child {
result = append(result, v)
}
return result
}
......@@ -2,12 +2,16 @@ package utils
import (
"fmt"
"slices"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/tree"
"github.com/emirpasic/gods/v2/maps/treemap"
"github.com/shirou/gopsutil/v4/process"
)
func TestRegexp(t *testing.T) {
......@@ -135,3 +139,59 @@ func TestChartype(t *testing.T) {
t.Logf("%v: %d", i.Key(), i.Value())
}
}
func ff(t *tree.Tree, root *ProcessInfo, ctx []int32) []int32 {
ctx = append(ctx, root.Pid)
pids := make([]int32, 0, len(root.Child))
for k := range root.Child {
pids = append(pids, k)
}
slices.Sort(pids)
for _, v := range pids {
c := root.Child[v]
tt := tree.New().Root(fmt.Sprintf("%d: %s", c.Pid, c.Cmd))
t.Child(tt)
if len(c.Child) > 0 {
ctx = ff(tt, c, ctx)
} else {
ctx = append(ctx, c.Pid)
}
}
return ctx
}
func TestTree(t *testing.T) {
start := time.Now()
_, root := GetPsTree([]int32{
2925089, 2925087, 2947677, 2925085, 2927231, 2925091, 2927238, 2927228, 2927236, 2927226, 2924226, 2925088, 2947517, 2927224, 2924385, 2925086, 2924872, 2925092, 2925090, 2927219, 2927237,
})
d := time.Since(start)
tree := tree.New()
tree.Root(fmt.Sprintf("%d: %s", root.Pid, root.Cmd))
t.Logf("%d ms\n", d.Milliseconds())
so := make([]int32, 0, 128)
so = ff(tree, root, so)
d = time.Since(start)
t.Logf("%d ms\n", d.Milliseconds())
st := lipgloss.NewStyle().MaxWidth(100)
t.Logf("\n%s", st.Render(tree.String()))
t.Logf("%+v \n", so)
}
func TestAddChilds(t *testing.T) {
style := lipgloss.NewStyle().Inline(true).MaxWidth(10).Width(10)
t.Logf("|%s|", style.Render("01234567890123456789"))
t.Logf("|%s|", style.Align(lipgloss.Center).Render("01234567"))
}
func TestChild(t *testing.T) {
p, _ := process.NewProcess(2810755)
start := time.Now()
childs, err := p.Children()
d := time.Since(start)
if err != nil {
t.Error(err)
}
t.Logf("%d ms \n", d.Milliseconds())
t.Logf("childs num: %d", len(childs))
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment