mirror of
https://github.com/yusing/godoxy.git
synced 2026-03-18 07:13:50 +01:00
This major overhaul of the idlewatcher system introduces a modern, real-time loading experience with Server-Sent Events (SSE) streaming and improved error handling. - **Real-time Event Streaming**: New SSE endpoint (`/$godoxy/wake-events`) provides live updates during container wake process - **Enhanced Loading Page**: Modern console-style interface with timestamped events and color-coded status messages - **Improved Static Asset Management**: Dedicated paths for CSS, JS, and favicon to avoid conflicting with upstream assets - **Event History Buffer**: Stores wake events for reconnecting clients and debugging - Refactored HTTP request handling with cleaner static asset routing - Added `WakeEvent` system with structured event types (starting, waking_dep, dep_ready, container_woke, waiting_ready, ready, error) - Implemented thread-safe event broadcasting using xsync.Map for concurrent SSE connections - Enhanced error handling with detailed logging and user-friendly error messages - Simplified loading page template system with better asset path management - Fixed race conditions in dependency waking and state management - Removed `common.go` functions (canceled, waitStarted) - moved inline for better context - Updated Waker interface to accept context parameter in Wake() method - New static asset paths use `/$godoxy/` prefix to avoid conflicts - Console-style output with Fira Code font for better readability - Color-coded event types (yellow for starting, blue for dependencies, green for success, red for errors) - Automatic page refresh when container becomes ready - Improved visual design with better glassmorphism effects and responsive layout - Real-time progress feedback during dependency wake and container startup This change transforms the static loading page into a dynamic, informative experience that keeps users informed during the wake process while maintaining backward compatibility with existing routing behavior.
203 lines
4.8 KiB
Go
Executable File
203 lines
4.8 KiB
Go
Executable File
package route
|
|
|
|
import (
|
|
"net/http"
|
|
"sync"
|
|
|
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
|
"github.com/yusing/godoxy/agent/pkg/agentproxy"
|
|
"github.com/yusing/godoxy/internal/idlewatcher"
|
|
"github.com/yusing/godoxy/internal/logging/accesslog"
|
|
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
|
"github.com/yusing/godoxy/internal/net/gphttp/loadbalancer"
|
|
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
|
nettypes "github.com/yusing/godoxy/internal/net/types"
|
|
"github.com/yusing/godoxy/internal/route/routes"
|
|
"github.com/yusing/godoxy/internal/types"
|
|
"github.com/yusing/godoxy/internal/watcher/health/monitor"
|
|
gperr "github.com/yusing/goutils/errs"
|
|
"github.com/yusing/goutils/http/reverseproxy"
|
|
"github.com/yusing/goutils/task"
|
|
"github.com/yusing/goutils/version"
|
|
)
|
|
|
|
type ReveseProxyRoute struct {
|
|
*Route
|
|
|
|
loadBalancer *loadbalancer.LoadBalancer
|
|
handler http.Handler
|
|
rp *reverseproxy.ReverseProxy
|
|
}
|
|
|
|
var _ types.ReverseProxyRoute = (*ReveseProxyRoute)(nil)
|
|
|
|
// var globalMux = http.NewServeMux() // TODO: support regex subdomain matching.
|
|
|
|
func NewReverseProxyRoute(base *Route) (*ReveseProxyRoute, gperr.Error) {
|
|
httpConfig := base.HTTPConfig
|
|
proxyURL := base.ProxyURL
|
|
|
|
var trans *http.Transport
|
|
a := base.GetAgent()
|
|
if a != nil {
|
|
trans = a.Transport()
|
|
proxyURL = nettypes.NewURL(agent.HTTPProxyURL)
|
|
} else {
|
|
tlsConfig, err := httpConfig.BuildTLSConfig(&base.ProxyURL.URL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
trans = gphttp.NewTransportWithTLSConfig(tlsConfig)
|
|
if httpConfig.ResponseHeaderTimeout > 0 {
|
|
trans.ResponseHeaderTimeout = httpConfig.ResponseHeaderTimeout
|
|
}
|
|
if httpConfig.DisableCompression {
|
|
trans.DisableCompression = true
|
|
}
|
|
}
|
|
|
|
service := base.Name()
|
|
rp := reverseproxy.NewReverseProxy(service, &proxyURL.URL, trans)
|
|
|
|
if len(base.Middlewares) > 0 {
|
|
err := middleware.PatchReverseProxy(rp, base.Middlewares)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if a != nil {
|
|
cfg := agentproxy.Config{
|
|
Scheme: base.ProxyURL.Scheme,
|
|
Host: base.ProxyURL.Host,
|
|
HTTPConfig: httpConfig,
|
|
}
|
|
setHeaderFunc := cfg.SetAgentProxyConfigHeaders
|
|
if !a.Version.IsOlderThan(version.New(0, 18, 6)) {
|
|
setHeaderFunc = cfg.SetAgentProxyConfigHeadersLegacy
|
|
}
|
|
|
|
ori := rp.HandlerFunc
|
|
rp.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
|
|
setHeaderFunc(r.Header)
|
|
ori(w, r)
|
|
}
|
|
}
|
|
|
|
r := &ReveseProxyRoute{
|
|
Route: base,
|
|
rp: rp,
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// ReverseProxy implements routes.ReverseProxyRoute.
|
|
func (r *ReveseProxyRoute) ReverseProxy() *reverseproxy.ReverseProxy {
|
|
return r.rp
|
|
}
|
|
|
|
// Start implements task.TaskStarter.
|
|
func (r *ReveseProxyRoute) Start(parent task.Parent) gperr.Error {
|
|
r.task = parent.Subtask("http."+r.Name(), false)
|
|
|
|
switch {
|
|
case r.UseIdleWatcher():
|
|
waker, err := idlewatcher.NewWatcher(parent, r, r.IdlewatcherConfig())
|
|
if err != nil {
|
|
r.task.Finish(err)
|
|
return gperr.Wrap(err)
|
|
}
|
|
r.handler = waker
|
|
r.HealthMon = waker
|
|
case r.UseHealthCheck():
|
|
r.HealthMon = monitor.NewMonitor(r)
|
|
}
|
|
|
|
if r.handler == nil {
|
|
r.handler = r.rp
|
|
}
|
|
|
|
if r.UseAccessLog() {
|
|
var err error
|
|
r.rp.AccessLogger, err = accesslog.NewAccessLogger(r.task, r.AccessLog)
|
|
if err != nil {
|
|
r.task.Finish(err)
|
|
return gperr.Wrap(err)
|
|
}
|
|
}
|
|
|
|
if len(r.Rules) > 0 {
|
|
r.handler = r.Rules.BuildHandler(r.handler.ServeHTTP)
|
|
}
|
|
|
|
if r.HealthMon != nil {
|
|
if err := r.HealthMon.Start(r.task); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if r.ShouldExclude() {
|
|
return nil
|
|
}
|
|
|
|
if r.UseLoadBalance() {
|
|
r.addToLoadBalancer(parent)
|
|
} else {
|
|
routes.HTTP.Add(r)
|
|
r.task.OnCancel("remove_route_from_http", func() {
|
|
routes.HTTP.Del(r)
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *ReveseProxyRoute) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
// req.Header.Set("Accept-Encoding", "identity")
|
|
r.handler.ServeHTTP(w, req)
|
|
}
|
|
|
|
var lbLock sync.Mutex
|
|
|
|
func (r *ReveseProxyRoute) addToLoadBalancer(parent task.Parent) {
|
|
var lb *loadbalancer.LoadBalancer
|
|
cfg := r.LoadBalance
|
|
lbLock.Lock()
|
|
|
|
l, ok := routes.HTTP.Get(cfg.Link)
|
|
var linked *ReveseProxyRoute
|
|
if ok {
|
|
lbLock.Unlock()
|
|
linked = l.(*ReveseProxyRoute)
|
|
lb = linked.loadBalancer
|
|
lb.UpdateConfigIfNeeded(cfg)
|
|
if linked.Homepage.Name == "" {
|
|
linked.Homepage = r.Homepage
|
|
}
|
|
} else {
|
|
lb = loadbalancer.New(cfg)
|
|
_ = lb.Start(parent) // always return nil
|
|
linked = &ReveseProxyRoute{
|
|
Route: &Route{
|
|
Alias: cfg.Link,
|
|
Homepage: r.Homepage,
|
|
},
|
|
loadBalancer: lb,
|
|
handler: lb,
|
|
}
|
|
linked.SetHealthMonitor(lb)
|
|
routes.HTTP.AddKey(cfg.Link, linked)
|
|
r.task.OnFinished("remove_loadbalancer_route", func() {
|
|
routes.HTTP.DelKey(cfg.Link)
|
|
})
|
|
lbLock.Unlock()
|
|
}
|
|
r.loadBalancer = lb
|
|
|
|
server := loadbalancer.NewServer(r.task.Name(), r.ProxyURL, r.LoadBalance.Weight, r.handler, r.HealthMon)
|
|
lb.AddServer(server)
|
|
r.task.OnCancel("lb_remove_server", func() {
|
|
lb.RemoveServer(server)
|
|
})
|
|
}
|