initial commit
This commit is contained in:
15
go.mod
Normal file
15
go.mod
Normal file
@@ -0,0 +1,15 @@
|
||||
module processSupervisor
|
||||
|
||||
go 1.24.5
|
||||
|
||||
require (
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // 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/sys v0.20.0 // indirect
|
||||
)
|
24
go.sum
Normal file
24
go.sum
Normal file
@@ -0,0 +1,24 @@
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
67
handlers/htopHandler.go
Normal file
67
handlers/htopHandler.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"processSupervisor/models"
|
||||
)
|
||||
|
||||
func HtopHandler(w http.ResponseWriter, r *http.Request) {
|
||||
table, err := models.GetTable()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get processes", 500)
|
||||
return
|
||||
}
|
||||
table.Sort(r)
|
||||
|
||||
funcMap := template.FuncMap{
|
||||
"div": divide,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
// Detect HTMX request via the HX-Request header
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
tmpl := template.Must(
|
||||
template.New("table.html").Funcs(funcMap).ParseFiles("templates/partials/table.html"),
|
||||
)
|
||||
tmpl.Execute(w, table)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl := template.Must(
|
||||
template.New("htop.html").Funcs(funcMap).ParseFiles(
|
||||
"templates/htop.html",
|
||||
"templates/partials/table.html",
|
||||
),
|
||||
)
|
||||
tmpl.Execute(w, table)
|
||||
}
|
||||
|
||||
func divide(a any, b any) float64 {
|
||||
af := toFloat64(a)
|
||||
bf := toFloat64(b)
|
||||
if bf == 0 {
|
||||
return 0
|
||||
}
|
||||
return af / bf
|
||||
}
|
||||
|
||||
func toFloat64(v any) float64 {
|
||||
switch val := v.(type) {
|
||||
case int:
|
||||
return float64(val)
|
||||
case int32:
|
||||
return float64(val)
|
||||
case int64:
|
||||
return float64(val)
|
||||
case uint64:
|
||||
return float64(val)
|
||||
case float32:
|
||||
return float64(val)
|
||||
case float64:
|
||||
return val
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
40
handlers/killHandler.go
Normal file
40
handlers/killHandler.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"processSupervisor/models"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func KillHandler(w http.ResponseWriter, r *http.Request) {
|
||||
pidStr := r.URL.Query().Get("pid")
|
||||
pid, err := strconv.Atoi(pidStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid PID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
proc, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
http.Error(w, "Process not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
err = proc.Kill()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to kill process", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Return updated table partial (same as HTMX request)
|
||||
processes, err := models.GetProcesses()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get processes", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl := template.Must(template.ParseFiles("templates/partials/table.html"))
|
||||
tmpl.Execute(w, processes)
|
||||
}
|
18
main.go
Normal file
18
main.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"processSupervisor/handlers"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fs := http.FileServer(http.Dir("static"))
|
||||
http.Handle("/static/", http.StripPrefix("/static/", fs))
|
||||
|
||||
http.HandleFunc("/taskmanager/htop", handlers.HtopHandler)
|
||||
http.HandleFunc("/taskmanager/kill", handlers.KillHandler)
|
||||
|
||||
log.Println("Listening on http://localhost:8080/taskmanager/htop")
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
17
models/htopTable.go
Normal file
17
models/htopTable.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package models
|
||||
|
||||
type HtopTable struct {
|
||||
Processes []Process
|
||||
CurrentSort string
|
||||
CurrentOrder string
|
||||
}
|
||||
|
||||
func GetTable() (*HtopTable, error) {
|
||||
processes, err := GetProcesses()
|
||||
if err != nil {
|
||||
return &HtopTable{}, err
|
||||
}
|
||||
return &HtopTable{
|
||||
Processes: processes,
|
||||
}, nil
|
||||
}
|
108
models/process.go
Normal file
108
models/process.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
ps "github.com/shirou/gopsutil/v3/process"
|
||||
)
|
||||
|
||||
type Process struct {
|
||||
PID string
|
||||
User string
|
||||
Cmd string
|
||||
CPU float64
|
||||
Memory uint64
|
||||
}
|
||||
|
||||
func GetProcesses() ([]Process, error) {
|
||||
processes, err := ps.Processes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []Process
|
||||
for _, p := range processes {
|
||||
pid := p.Pid
|
||||
|
||||
name, err := p.Name()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
user, err := p.Username()
|
||||
if err != nil {
|
||||
user = "unknown"
|
||||
}
|
||||
|
||||
cpu, err := p.CPUPercent()
|
||||
if err != nil {
|
||||
cpu = 0
|
||||
}
|
||||
|
||||
memInfo, err := p.MemoryInfo()
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, Process{
|
||||
PID: fmt.Sprintf("%d", pid),
|
||||
User: user,
|
||||
Cmd: name,
|
||||
CPU: cpu,
|
||||
Memory: memInfo.RSS,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (t *HtopTable) Sort(r *http.Request) {
|
||||
sortBy := r.URL.Query().Get("sort")
|
||||
order := r.URL.Query().Get("order")
|
||||
|
||||
if order == "desc" {
|
||||
t.CurrentOrder = "desc"
|
||||
} else {
|
||||
t.CurrentOrder = "asc"
|
||||
}
|
||||
|
||||
t.CurrentSort = sortBy
|
||||
|
||||
sort.Slice(t.Processes, func(i, j int) bool {
|
||||
switch sortBy {
|
||||
case "pid":
|
||||
pidI, _ := strconv.Atoi(t.Processes[i].PID)
|
||||
pidJ, _ := strconv.Atoi(t.Processes[j].PID)
|
||||
if t.CurrentOrder == "desc" {
|
||||
return pidI > pidJ
|
||||
}
|
||||
return pidI < pidJ
|
||||
case "user":
|
||||
if t.CurrentOrder == "desc" {
|
||||
return t.Processes[i].User > t.Processes[j].User
|
||||
}
|
||||
return t.Processes[i].User < t.Processes[j].User
|
||||
case "cmd":
|
||||
if t.CurrentOrder == "desc" {
|
||||
return t.Processes[i].Cmd > t.Processes[j].Cmd
|
||||
}
|
||||
return t.Processes[i].Cmd < t.Processes[j].Cmd
|
||||
case "cpu":
|
||||
if t.CurrentOrder == "desc" {
|
||||
return t.Processes[i].CPU > t.Processes[j].CPU
|
||||
}
|
||||
return t.Processes[i].CPU < t.Processes[j].CPU
|
||||
case "memory":
|
||||
if t.CurrentOrder == "desc" {
|
||||
return t.Processes[i].Memory > t.Processes[j].Memory
|
||||
}
|
||||
return t.Processes[i].Memory < t.Processes[j].Memory
|
||||
default:
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
1
static/htmx.min.js
vendored
Normal file
1
static/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
110
templates/htop.html
Normal file
110
templates/htop.html
Normal file
@@ -0,0 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Task Manager</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #121212;
|
||||
--text-color: #e0e0e0;
|
||||
--accent-color: #007bff;
|
||||
--danger-color: #dc3545;
|
||||
--table-bg: #1e1e1e;
|
||||
--hover-bg: #2a2a2a;
|
||||
--border-color: #333;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
text-align: center;
|
||||
font-size: 2.8rem;
|
||||
margin: 2rem 0 1rem 0;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
background: linear-gradient(to right, #00c6ff, #00e6aa);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-shadow: 0 0 5px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #1e1e2f; /* dark card background */
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.5);
|
||||
margin: 3rem auto; /* vertical margin + horizontal centering */
|
||||
max-width: 900px; /* limits width */
|
||||
color: #e0e0ff;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background-color: var(--table-bg);
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.3);
|
||||
margin-top: 1rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 10px 15px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: var(--accent-color);
|
||||
color: white;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: var(--hover-bg);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 5px 10px;
|
||||
background-color: var(--danger-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #80dfff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 class="page-title">🧠 Task Manager</h1>
|
||||
<div
|
||||
id="htop-wrapper"
|
||||
hx-get="/taskmanager/htop?sort={{.CurrentSort}}&order={{.CurrentOrder}}"
|
||||
hx-trigger="load, every 1s"
|
||||
hx-target="#process-table"
|
||||
hx-swap="innerHTML">
|
||||
|
||||
<div id="process-table" class="card">
|
||||
{{template "table.html" .}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
70
templates/partials/table.html
Normal file
70
templates/partials/table.html
Normal file
@@ -0,0 +1,70 @@
|
||||
<div id="process-table" class="card">
|
||||
<table >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a hx-get="/taskmanager/htop?sort=pid&order={{if and (eq .CurrentSort "pid") (eq .CurrentOrder "asc")}}desc{{else}}asc{{end}}"
|
||||
hx-target="#process-table"
|
||||
hx-swap="outerHTML">
|
||||
PID {{if eq .CurrentSort "pid"}}{{if eq .CurrentOrder "asc"}}↑{{else}}↓{{end}}{{end}}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a hx-get="/taskmanager/htop?sort=user&order={{if and (eq .CurrentSort "user") (eq .CurrentOrder "asc")}}desc{{else}}asc{{end}}"
|
||||
hx-target="#process-table"
|
||||
hx-swap="outerHTML">
|
||||
User {{if eq .CurrentSort "user"}}{{if eq .CurrentOrder "asc"}}↑{{else}}↓{{end}}{{end}}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a hx-get="/taskmanager/htop?sort=cmd&order={{if and (eq .CurrentSort "cmd") (eq .CurrentOrder "asc")}}desc{{else}}asc{{end}}"
|
||||
hx-target="#process-table"
|
||||
hx-swap="outerHTML">
|
||||
Command {{if eq .CurrentSort "cmd"}}{{if eq .CurrentOrder "asc"}}↑{{else}}↓{{end}}{{end}}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a hx-get="/taskmanager/htop?sort=cpu&order={{if and (eq .CurrentSort "cpu") (eq .CurrentOrder "asc")}}desc{{else}}asc{{end}}"
|
||||
hx-target="#process-table"
|
||||
hx-swap="outerHTML">
|
||||
CPU %{{if eq .CurrentSort "cpu"}}{{if eq .CurrentOrder "asc"}}↑{{else}}↓{{end}}{{end}}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a hx-get="/taskmanager/htop?sort=memory&order={{if and (eq .CurrentSort "memory") (eq .CurrentOrder "asc")}}desc{{else}}asc{{end}}"
|
||||
hx-target="#process-table"
|
||||
hx-swap="outerHTML">
|
||||
Memory {{if eq .CurrentSort "memory"}}{{if eq .CurrentOrder "asc"}}↑{{else}}↓{{end}}{{end}}
|
||||
</a>
|
||||
</th>
|
||||
<th>Kill</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Processes}}
|
||||
<tr>
|
||||
<td class="pid">{{.PID}}</td>
|
||||
<td class="user">{{.User}}</td>
|
||||
<td class="cmd">{{.Cmd}}</td>
|
||||
<td class="cpu">{{printf "%.2f" .CPU}}</td>
|
||||
<td class="mem">
|
||||
{{if ge .Memory 1073741824}}
|
||||
{{printf "%.2f GB" (div .Memory 1073741824)}}
|
||||
{{else}}
|
||||
{{printf "%.0f MB" (div .Memory 1048576)}}
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="kill">
|
||||
<button
|
||||
hx-get="/taskmanager/kill?pid={{.PID}}"
|
||||
hx-target="#process-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Are you sure you want to kill this process?">
|
||||
Kill
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
Reference in New Issue
Block a user