From 09702266a91a0f5e65441cb530542bfeda3942f0 Mon Sep 17 00:00:00 2001 From: yusing Date: Mon, 22 Dec 2025 16:57:47 +0800 Subject: [PATCH] feat(debug): implement debug server for development environment - Added `listenDebugServer` function to handle debug requests. - Introduced table based debug page with different functionalities. - Updated Makefile to use `scc` for code analysis instead of `cloc`. --- Makefile | 4 +- cmd/debug_page.go | 257 ++++++++++++++++++++++ cmd/debug_page_prod.go | 7 + cmd/main.go | 2 + internal/idlewatcher/handle_http_debug.go | 73 ++++++ internal/idlewatcher/types/paths.go | 7 +- 6 files changed, 345 insertions(+), 5 deletions(-) create mode 100644 cmd/debug_page.go create mode 100644 cmd/debug_page_prod.go create mode 100644 internal/idlewatcher/handle_http_debug.go diff --git a/Makefile b/Makefile index 8b9bcb84..1a99ee1b 100755 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ else ifeq ($(debug), 1) CGO_ENABLED = 1 GODOXY_DEBUG = 1 GO_TAGS += debug - BUILD_FLAGS += -asan # FIXME: -gcflags=all='-N -l' + # FIXME: BUILD_FLAGS += -asan -gcflags=all='-N -l' else ifeq ($(pprof), 1) CGO_ENABLED = 0 GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/ halt_on_error=1 @@ -142,7 +142,7 @@ ci-test: act -n --artifact-server-path /tmp/artifacts -s GITHUB_TOKEN="$$(gh auth token)" cloc: - cloc --include-lang=Go --not-match-f '_test.go$$' . + scc -w -i go --not-match '_test.go$' push-github: git push origin $(shell git rev-parse --abbrev-ref HEAD) diff --git a/cmd/debug_page.go b/cmd/debug_page.go new file mode 100644 index 00000000..00d44b80 --- /dev/null +++ b/cmd/debug_page.go @@ -0,0 +1,257 @@ +//go:build !production + +package main + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/yusing/godoxy/internal/api" + apiV1 "github.com/yusing/godoxy/internal/api/v1" + agentApi "github.com/yusing/godoxy/internal/api/v1/agent" + authApi "github.com/yusing/godoxy/internal/api/v1/auth" + certApi "github.com/yusing/godoxy/internal/api/v1/cert" + dockerApi "github.com/yusing/godoxy/internal/api/v1/docker" + fileApi "github.com/yusing/godoxy/internal/api/v1/file" + homepageApi "github.com/yusing/godoxy/internal/api/v1/homepage" + metricsApi "github.com/yusing/godoxy/internal/api/v1/metrics" + routeApi "github.com/yusing/godoxy/internal/api/v1/route" + "github.com/yusing/godoxy/internal/auth" + "github.com/yusing/godoxy/internal/idlewatcher" + idlewatcherTypes "github.com/yusing/godoxy/internal/idlewatcher/types" +) + +type debugMux struct { + endpoints []debugEndpoint + mux http.ServeMux +} + +type debugEndpoint struct { + name string + method string + path string +} + +func newDebugMux() *debugMux { + return &debugMux{ + endpoints: make([]debugEndpoint, 0), + mux: *http.NewServeMux(), + } +} + +func (mux *debugMux) registerEndpoint(name, method, path string) { + mux.endpoints = append(mux.endpoints, debugEndpoint{name: name, method: method, path: path}) +} + +func (mux *debugMux) HandleFunc(name, method, path string, handler http.HandlerFunc) { + mux.registerEndpoint(name, method, path) + mux.mux.HandleFunc(method+" "+path, handler) +} + +func (mux *debugMux) Finalize() { + mux.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, ` + + + + + + + + + + + + + + + `) + for _, endpoint := range mux.endpoints { + fmt.Fprintf(w, "", endpoint.path, endpoint.name, endpoint.method, endpoint.path) + } + fmt.Fprintln(w, ` + +
NameMethodPath
%s%s%s
+ +`) + }) +} + +func listenDebugServer() { + mux := newDebugMux() + mux.mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "image/svg+xml") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`🐙`)) + }) + + mux.HandleFunc("Auth block page", "GET", "/auth/block", AuthBlockPageHandler) + mux.HandleFunc("Idlewatcher loading page", "GET", idlewatcherTypes.PathPrefix, idlewatcher.DebugHandler) + apiHandler := newApiHandler(mux) + mux.mux.HandleFunc("/api/v1/", apiHandler.ServeHTTP) + + mux.Finalize() + + go http.ListenAndServe(":7777", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Expires", "0") + mux.mux.ServeHTTP(w, r) + })) +} + +func newApiHandler(debugMux *debugMux) *gin.Engine { + r := gin.New() + r.Use(api.ErrorHandler()) + r.Use(api.ErrorLoggingMiddleware()) + r.Use(api.NoCache()) + + registerGinRoute := func(router gin.IRouter, method, name string, path string, handler gin.HandlerFunc) { + if group, ok := router.(*gin.RouterGroup); ok { + debugMux.registerEndpoint(name, method, group.BasePath()+path) + } else { + debugMux.registerEndpoint(name, method, path) + } + router.Handle(method, path, handler) + } + + registerGinRoute(r, "GET", "App version", "/api/v1/version", apiV1.Version) + + v1 := r.Group("/api/v1") + if auth.IsEnabled() { + v1Auth := v1.Group("/auth") + { + registerGinRoute(v1Auth, "HEAD", "Auth check", "/check", authApi.Check) + registerGinRoute(v1Auth, "POST", "Auth login", "/login", authApi.Login) + registerGinRoute(v1Auth, "GET", "Auth callback", "/callback", authApi.Callback) + registerGinRoute(v1Auth, "POST", "Auth callback", "/callback", authApi.Callback) + registerGinRoute(v1Auth, "POST", "Auth logout", "/logout", authApi.Logout) + registerGinRoute(v1Auth, "GET", "Auth logout", "/logout", authApi.Logout) + } + } + + { + // enable cache for favicon + registerGinRoute(v1, "GET", "Route favicon", "/favicon", apiV1.FavIcon) + registerGinRoute(v1, "GET", "Route health", "/health", apiV1.Health) + registerGinRoute(v1, "GET", "List icons", "/icons", apiV1.Icons) + registerGinRoute(v1, "POST", "Config reload", "/reload", apiV1.Reload) + registerGinRoute(v1, "GET", "Route stats", "/stats", apiV1.Stats) + + route := v1.Group("/route") + { + registerGinRoute(route, "GET", "List routes", "/list", routeApi.Routes) + registerGinRoute(route, "GET", "Get route", "/:which", routeApi.Route) + registerGinRoute(route, "GET", "List providers", "/providers", routeApi.Providers) + registerGinRoute(route, "GET", "List routes by provider", "/by_provider", routeApi.ByProvider) + registerGinRoute(route, "POST", "Playground", "/playground", routeApi.Playground) + } + + file := v1.Group("/file") + { + registerGinRoute(file, "GET", "List files", "/list", fileApi.List) + registerGinRoute(file, "GET", "Get file", "/content", fileApi.Get) + registerGinRoute(file, "PUT", "Set file", "/content", fileApi.Set) + registerGinRoute(file, "POST", "Set file", "/content", fileApi.Set) + registerGinRoute(file, "POST", "Validate file", "/validate", fileApi.Validate) + } + + homepage := v1.Group("/homepage") + { + registerGinRoute(homepage, "GET", "List categories", "/categories", homepageApi.Categories) + registerGinRoute(homepage, "GET", "List items", "/items", homepageApi.Items) + registerGinRoute(homepage, "POST", "Set item", "/set/item", homepageApi.SetItem) + registerGinRoute(homepage, "POST", "Set items batch", "/set/items_batch", homepageApi.SetItemsBatch) + registerGinRoute(homepage, "POST", "Set item visible", "/set/item_visible", homepageApi.SetItemVisible) + registerGinRoute(homepage, "POST", "Set item favorite", "/set/item_favorite", homepageApi.SetItemFavorite) + registerGinRoute(homepage, "POST", "Set item sort order", "/set/item_sort_order", homepageApi.SetItemSortOrder) + registerGinRoute(homepage, "POST", "Set item all sort order", "/set/item_all_sort_order", homepageApi.SetItemAllSortOrder) + registerGinRoute(homepage, "POST", "Set item fav sort order", "/set/item_fav_sort_order", homepageApi.SetItemFavSortOrder) + registerGinRoute(homepage, "POST", "Set category order", "/set/category_order", homepageApi.SetCategoryOrder) + registerGinRoute(homepage, "POST", "Item click", "/item_click", homepageApi.ItemClick) + } + + cert := v1.Group("/cert") + { + registerGinRoute(cert, "GET", "Get cert info", "/info", certApi.Info) + registerGinRoute(cert, "GET", "Renew cert", "/renew", certApi.Renew) + } + + agent := v1.Group("/agent") + { + registerGinRoute(agent, "GET", "List agents", "/list", agentApi.List) + registerGinRoute(agent, "POST", "Create agent", "/create", agentApi.Create) + registerGinRoute(agent, "POST", "Verify agent", "/verify", agentApi.Verify) + } + + metrics := v1.Group("/metrics") + { + registerGinRoute(metrics, "GET", "Get system info", "/system_info", metricsApi.SystemInfo) + registerGinRoute(metrics, "GET", "Get all system info", "/all_system_info", metricsApi.AllSystemInfo) + registerGinRoute(metrics, "GET", "Get uptime", "/uptime", metricsApi.Uptime) + } + + docker := v1.Group("/docker") + { + registerGinRoute(docker, "GET", "Get container", "/container/:id", dockerApi.GetContainer) + registerGinRoute(docker, "GET", "List containers", "/containers", dockerApi.Containers) + registerGinRoute(docker, "GET", "Get docker info", "/info", dockerApi.Info) + registerGinRoute(docker, "GET", "Get docker logs", "/logs/:id", dockerApi.Logs) + registerGinRoute(docker, "POST", "Start docker container", "/start", dockerApi.Start) + registerGinRoute(docker, "POST", "Stop docker container", "/stop", dockerApi.Stop) + registerGinRoute(docker, "POST", "Restart docker container", "/restart", dockerApi.Restart) + } + } + + return r +} + +func AuthBlockPageHandler(w http.ResponseWriter, r *http.Request) { + auth.WriteBlockPage(w, http.StatusForbidden, "Forbidden", "Login", "/login") +} diff --git a/cmd/debug_page_prod.go b/cmd/debug_page_prod.go new file mode 100644 index 00000000..8d04e82e --- /dev/null +++ b/cmd/debug_page_prod.go @@ -0,0 +1,7 @@ +//go:build production + +package main + +func listenDebugServer() { + // no-op +} diff --git a/cmd/main.go b/cmd/main.go index c56d6bc5..3c8e9694 100755 --- a/cmd/main.go +++ b/cmd/main.go @@ -72,6 +72,8 @@ func main() { Handler: api.NewHandler(), }) + listenDebugServer() + uptime.Poller.Start() config.WatchChanges() diff --git a/internal/idlewatcher/handle_http_debug.go b/internal/idlewatcher/handle_http_debug.go new file mode 100644 index 00000000..b0b144fc --- /dev/null +++ b/internal/idlewatcher/handle_http_debug.go @@ -0,0 +1,73 @@ +//go:build !production + +package idlewatcher + +import ( + "math/rand/v2" + "net/http" + "time" + + "github.com/puzpuzpuz/xsync/v4" + idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types" + "github.com/yusing/godoxy/internal/types" +) + +func DebugHandler(rw http.ResponseWriter, r *http.Request) { + w := &Watcher{ + eventChs: xsync.NewMap[chan *WakeEvent, struct{}](), + cfg: &types.IdlewatcherConfig{ + IdlewatcherProviderConfig: types.IdlewatcherProviderConfig{ + Docker: &types.DockerConfig{ + ContainerName: "test", + }, + }, + }, + } + + switch r.URL.Path { + case idlewatcher.LoadingPageCSSPath: + serveStaticContent(rw, http.StatusOK, "text/css", cssBytes) + case idlewatcher.LoadingPageJSPath: + serveStaticContent(rw, http.StatusOK, "application/javascript", jsBytes) + case idlewatcher.WakeEventsPath: + go w.handleWakeEventsSSE(rw, r) + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + events := []WakeEventType{ + WakeEventStarting, + WakeEventWakingDep, + WakeEventDepReady, + WakeEventContainerWoke, + WakeEventWaitingReady, + WakeEventError, + WakeEventReady, + } + messages := []string{ + "Starting", + "Waking dependency", + "Dependency ready", + "Container woke", + "Waiting for ready", + "Error", + "Ready", + } + + for { + select { + case <-r.Context().Done(): + return + case <-ticker.C: + idx := rand.IntN(len(events)) + for ch := range w.eventChs.Range { + ch <- &WakeEvent{ + Type: string(events[idx]), + Message: messages[idx], + Timestamp: time.Now(), + } + } + } + } + default: + w.writeLoadingPage(rw) + } +} diff --git a/internal/idlewatcher/types/paths.go b/internal/idlewatcher/types/paths.go index 88606874..622c9b7c 100644 --- a/internal/idlewatcher/types/paths.go +++ b/internal/idlewatcher/types/paths.go @@ -2,7 +2,8 @@ package idlewatcher const ( FavIconPath = "/favicon.ico" - LoadingPageCSSPath = "/$godoxy/style.css" - LoadingPageJSPath = "/$godoxy/loading.js" - WakeEventsPath = "/$godoxy/wake-events" + PathPrefix = "/$godoxy/" + LoadingPageCSSPath = PathPrefix + "style.css" + LoadingPageJSPath = PathPrefix + "loading.js" + WakeEventsPath = PathPrefix + "wake-events" )