diff --git a/api/api.go b/api/api.go index 10531f8..e02d818 100644 --- a/api/api.go +++ b/api/api.go @@ -1,10 +1,12 @@ package api import ( + "encoding/json" "fmt" "html/template" "log" "net/http" + "nvidiadashboard/pkg/constant" "nvidiadashboard/pkg/nvidia" "os" "path/filepath" @@ -37,6 +39,7 @@ type ( Username string DriverVersion string CUDAVersion string + Version string } ) @@ -72,6 +75,7 @@ func New(repo NVIDIARepository) *Server { s.mux.Use(middleware.URLFormat) s.mux.Get("/", s.handleRoot) + s.mux.Get("/{uuid:GPU-(.*)}/json", s.handleGPUJSON) s.mux.Get("/{uuid:GPU-(.*)}", s.handleGPU) s.mux.Get("/{path:(.*).html}", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) @@ -95,7 +99,7 @@ func (s *Server) Serve(port uint) error { func (s *Server) handleRoot(w http.ResponseWriter, _ *http.Request) { gpus, err := s.repo.GetGPUs() if err != nil { - sendInternalServerErrorHTML(w, err) + internalServerErrorHTML(w, err) return } if len(gpus) > 0 { @@ -105,11 +109,13 @@ func (s *Server) handleRoot(w http.ResponseWriter, _ *http.Request) { } wp := WebPack{ Username: "anonymous", + Version: constant.Version, } t, err := template.ParseFiles("static/no_gpu.html") if err != nil { - sendInternalServerErrorHTML(w, err) + internalServerErrorHTML(w, err) + return } err = t.Execute(w, wp) if err != nil { @@ -122,7 +128,7 @@ func (s *Server) handleGPU(w http.ResponseWriter, r *http.Request) { gpus, err := s.repo.GetGPUs() if err != nil { - sendInternalServerErrorHTML(w, err) + internalServerErrorHTML(w, err) return } i := -1 @@ -137,7 +143,7 @@ func (s *Server) handleGPU(w http.ResponseWriter, r *http.Request) { } gpu, err := s.repo.GetGPU(i) if err != nil { - sendInternalServerErrorHTML(w, err) + internalServerErrorHTML(w, err) return } wp := WebPack{ @@ -146,11 +152,13 @@ func (s *Server) handleGPU(w http.ResponseWriter, r *http.Request) { GPU: gpu, DriverVersion: s.repo.DriverVersion(), CUDAVersion: s.repo.CUDAVersion(), + Version: constant.Version, } t, err := template.New("index").Funcs(templateFuncMap).ParseFiles("static/index.html") if err != nil { - sendInternalServerErrorHTML(w, err) + internalServerErrorHTML(w, err) + return } err = t.ExecuteTemplate(w, "index.html", wp) if err != nil { @@ -158,6 +166,38 @@ func (s *Server) handleGPU(w http.ResponseWriter, r *http.Request) { } } +func (s *Server) handleGPUJSON(w http.ResponseWriter, r *http.Request) { + uuid := chi.URLParam(r, "uuid") + + gpus, err := s.repo.GetGPUs() + if err != nil { + internalServerErrorJSON(w, err) + return + } + i := -1 + for _, gpu := range gpus { + if gpu.UUID == uuid { + i = gpu.Index + } + } + if i == -1 { + notFoundJSON(w) + return + } + gpu, err := s.repo.GetGPU(i) + if err != nil { + internalServerErrorJSON(w, err) + return + } + body, err := json.Marshal(gpu) + if err != nil { + internalServerErrorJSON(w, err) + return + } + w.Header().Add("content-type", "application/json") + w.Write([]byte(body)) +} + // FileServer conveniently sets up a http.FileServer handler to serve // static files from a http.FileSystem. func fileServer(r chi.Router, path string, root http.FileSystem) { @@ -179,7 +219,7 @@ func fileServer(r chi.Router, path string, root http.FileSystem) { }) } -func sendInternalServerErrorHTML(w http.ResponseWriter, v any) { +func internalServerErrorHTML(w http.ResponseWriter, v any) { w.WriteHeader(http.StatusInternalServerError) body := fmt.Sprintf(internalServerErrorPage, v) w.Write([]byte(body)) @@ -191,6 +231,6 @@ func convertByteSize(v int) string { } func percentageRounded(a, b int) int { - p := (a / b) * 100 - return p + p := (float64(a) / float64(b)) * 100 + return int(p) } diff --git a/api/json.go b/api/json.go new file mode 100644 index 0000000..ce7ad4f --- /dev/null +++ b/api/json.go @@ -0,0 +1,58 @@ +package api + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "time" +) + +type ( + JSONBase struct { + Status int `json:"status"` + Timestamp time.Time `json:"timestamp"` + } + + JSONError struct { + JSONBase + Message string + } +) + +var ( + errLog = log.New(os.Stderr, log.Prefix(), log.Flags()) +) + +func internalServerErrorJSON(w http.ResponseWriter, v any) { + w.WriteHeader(http.StatusInternalServerError) + + body, err := json.Marshal(JSONError{ + JSONBase: JSONBase{ + Status: http.StatusInternalServerError, + Timestamp: time.Now(), + }, + Message: fmt.Sprintf("%v", v), + }) + if err != nil { + errLog.Println("[ERROR]", err) + } + w.Write([]byte(body)) +} + +func notFoundJSON(w http.ResponseWriter) { + w.WriteHeader(http.StatusNotFound) + + body, err := json.Marshal(JSONError{ + JSONBase: JSONBase{ + Status: http.StatusNotFound, + Timestamp: time.Now(), + }, + Message: "404 page not found", + }) + if err != nil { + errLog.Println("[ERROR]", err) + } + w.Write([]byte(body)) +} diff --git a/main.go b/main.go index 5c678c9..ba027bb 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,16 @@ package main import ( + "fmt" "log" "nvidiadashboard/api" + "nvidiadashboard/pkg/constant" "nvidiadashboard/pkg/nvidia" ) func main() { + fmt.Println("*** NVIDIA Web Dashboard -", constant.Version, "***") + r := nvidia.New() defer r.Close() diff --git a/pkg/constant/constant.go b/pkg/constant/constant.go new file mode 100644 index 0000000..5e27c22 --- /dev/null +++ b/pkg/constant/constant.go @@ -0,0 +1,5 @@ +package constant + +const ( + Version = "0.1-alpha" +) diff --git a/pkg/nvidia/nvidia.go b/pkg/nvidia/nvidia.go index 7654901..e6442eb 100644 --- a/pkg/nvidia/nvidia.go +++ b/pkg/nvidia/nvidia.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "strconv" + "strings" "github.com/NVIDIA/go-nvml/pkg/nvml" ) @@ -16,43 +17,43 @@ type ( } GPU struct { - Name string - UUID string - Index int + Name string `json:"name"` + UUID string `json:"uuid"` + Index int `json:"-"` } GPUDetail struct { GPU - CoreTemperature int - Utilization Utilization - Processes []Process - Memory Memory - Fans []Fan + CoreTemperature int `json:"coreTemperature"` + Utilization Utilization `json:"usage"` + Processes []Process `json:"processes"` + Memory Memory `json:"memory"` + Fans []Fan `json:"fans"` } Memory struct { - Free int - Reserved int - Total int - Used int - Version int + Free int `json:"free"` + Reserved int `json:"reserved"` + Total int `json:"total"` + Used int `json:"used"` + Version int `json:"-"` } Process struct { - PID int - MemoryUsed int - Name string - Type string + PID int `json:"pid"` + MemoryUsed int `json:"memoryUsed"` + Name string `json:"commandLine"` + Type string `json:"type"` } Utilization struct { - Decode int - Encode int - Rate int + Decoder int `json:"decoder"` + Encoder int `json:"encoder"` + Compute int `json:"compute"` } Fan struct { - Speed int + Speed int `json:"speed"` } ) @@ -196,9 +197,10 @@ func (*Repository) GetGPU(ID int) (GPUDetail, error) { } // Find process command line - for _, process := range allProcess { + for i, process := range allProcess { if cmdline, err := os.ReadFile("/proc/" + strconv.Itoa(process.PID) + "/cmdline"); err == nil { - process.Name = string(cmdline) + process.Name = strings.ReplaceAll(string(cmdline), "\u0000", " ") + allProcess[i] = process } } @@ -211,9 +213,9 @@ func (*Repository) GetGPU(ID int) (GPUDetail, error) { GPU: gpu, CoreTemperature: int(temp), Utilization: Utilization{ - Decode: int(decUsage), - Encode: int(encUsage), - Rate: int(load.Gpu), + Decoder: int(decUsage), + Encoder: int(encUsage), + Compute: int(load.Gpu), }, Processes: allProcess, Memory: Memory{ diff --git a/static b/static index de2d3dc..369059c 160000 --- a/static +++ b/static @@ -1 +1 @@ -Subproject commit de2d3dc85f6b20ff0884373250b6fd36a31f633a +Subproject commit 369059c925560f0d46f21cc51336eed419fb4ab7