diff --git a/internal/docker/client.go b/internal/docker/client.go index a276f610..f502ec57 100644 --- a/internal/docker/client.go +++ b/internal/docker/client.go @@ -37,12 +37,14 @@ var ( clientMapMu sync.RWMutex ) +var initClientCleanerOnce sync.Once + const ( cleanInterval = 10 * time.Second clientTTLSecs = int64(10) ) -func init() { +func initClientCleaner() { cleaner := task.RootTask("docker_clients_cleaner") go func() { ticker := time.NewTicker(cleanInterval) @@ -115,6 +117,8 @@ func (c *SharedClient) Close() { // - Client: the Docker client connection. // - error: an error if the connection failed. func NewClient(host string) (*SharedClient, error) { + initClientCleanerOnce.Do(initClientCleaner) + clientMapMu.Lock() defer clientMapMu.Unlock() diff --git a/internal/docker/idlewatcher/container.go b/internal/docker/idlewatcher/container.go index a0245fe6..1d8fbd0c 100644 --- a/internal/docker/idlewatcher/container.go +++ b/internal/docker/idlewatcher/container.go @@ -18,33 +18,41 @@ type ( } ) +func (w *Watcher) ContainerID() string { + return w.route.ContainerInfo().ContainerID +} + +func (w *Watcher) ContainerName() string { + return w.route.ContainerInfo().ContainerName +} + func (w *Watcher) containerStop(ctx context.Context) error { - return w.client.ContainerStop(ctx, w.ContainerID, container.StopOptions{ - Signal: string(w.StopSignal), - Timeout: &w.StopTimeout, + return w.client.ContainerStop(ctx, w.ContainerID(), container.StopOptions{ + Signal: string(w.Config().StopSignal), + Timeout: &w.Config().StopTimeout, }) } func (w *Watcher) containerPause(ctx context.Context) error { - return w.client.ContainerPause(ctx, w.ContainerID) + return w.client.ContainerPause(ctx, w.ContainerID()) } func (w *Watcher) containerKill(ctx context.Context) error { - return w.client.ContainerKill(ctx, w.ContainerID, string(w.StopSignal)) + return w.client.ContainerKill(ctx, w.ContainerID(), string(w.Config().StopSignal)) } func (w *Watcher) containerUnpause(ctx context.Context) error { - return w.client.ContainerUnpause(ctx, w.ContainerID) + return w.client.ContainerUnpause(ctx, w.ContainerID()) } func (w *Watcher) containerStart(ctx context.Context) error { - return w.client.ContainerStart(ctx, w.ContainerID, container.StartOptions{}) + return w.client.ContainerStart(ctx, w.ContainerID(), container.StartOptions{}) } func (w *Watcher) containerStatus() (string, error) { ctx, cancel := context.WithTimeoutCause(w.task.Context(), dockerReqTimeout, errors.New("docker request timeout")) defer cancel() - json, err := w.client.ContainerInspect(ctx, w.ContainerID) + json, err := w.client.ContainerInspect(ctx, w.ContainerID()) if err != nil { return "", err } diff --git a/internal/docker/idlewatcher/html/loading_page.html b/internal/docker/idlewatcher/html/loading_page.html index 405d23b6..0d655677 100644 --- a/internal/docker/idlewatcher/html/loading_page.html +++ b/internal/docker/idlewatcher/html/loading_page.html @@ -1,88 +1,139 @@ - + - - - - {{.Title}} - - - - -
-
{{.Message}}
- + /* Message Styles */ + .message { + font-size: 20px; + font-weight: 500; + text-align: center; + color: #f8f9fa; + max-width: 500px; + letter-spacing: 0.3px; + white-space: nowrap; + } + + /* Logo */ + .logo { + width: var(--logo-size); + height: var(--logo-size); + } + + + +
+ + +
+
+
+
+
+
{{.Message}}
+
+ + diff --git a/internal/docker/idlewatcher/loading_page.go b/internal/docker/idlewatcher/loading_page.go index b6e98609..3ece21ef 100644 --- a/internal/docker/idlewatcher/loading_page.go +++ b/internal/docker/idlewatcher/loading_page.go @@ -3,7 +3,6 @@ package idlewatcher import ( "bytes" _ "embed" - "strings" "text/template" "github.com/yusing/go-proxy/internal/net/gphttp/httpheaders" @@ -20,12 +19,12 @@ var loadingPage []byte var loadingPageTmpl = template.Must(template.New("loading_page").Parse(string(loadingPage))) func (w *Watcher) makeLoadingPageBody() []byte { - msg := w.ContainerName + " is starting..." + msg := w.ContainerName() + " is starting..." data := new(templateData) data.CheckRedirectHeader = httpheaders.HeaderGoDoxyCheckRedirect - data.Title = w.ContainerName - data.Message = strings.ReplaceAll(msg, " ", " ") + data.Title = w.route.HomepageItem().Name + data.Message = msg buf := bytes.NewBuffer(make([]byte, len(loadingPage)+len(data.Title)+len(data.Message)+len(httpheaders.HeaderGoDoxyCheckRedirect))) err := loadingPageTmpl.Execute(buf, data) diff --git a/internal/docker/idlewatcher/waker.go b/internal/docker/idlewatcher/waker.go index e535a2a5..ccda00e7 100644 --- a/internal/docker/idlewatcher/waker.go +++ b/internal/docker/idlewatcher/waker.go @@ -100,7 +100,7 @@ func (w *Watcher) Name() string { // String implements health.HealthMonitor. func (w *Watcher) String() string { - return w.ContainerName + return w.ContainerName() } // Uptime implements health.HealthMonitor. diff --git a/internal/docker/idlewatcher/waker_http.go b/internal/docker/idlewatcher/waker_http.go index 4658bc5b..3965713a 100644 --- a/internal/docker/idlewatcher/waker_http.go +++ b/internal/docker/idlewatcher/waker_http.go @@ -7,6 +7,7 @@ import ( "strconv" "time" + "github.com/yusing/go-proxy/internal/api/v1/favicon" gphttp "github.com/yusing/go-proxy/internal/net/gphttp" "github.com/yusing/go-proxy/internal/net/gphttp/httpheaders" ) @@ -55,6 +56,10 @@ func (w *Watcher) cancelled(reqCtx context.Context, rw http.ResponseWriter) bool } } +func isFaviconPath(path string) bool { + return path == "/favicon.ico" +} + func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldNext bool) { w.resetIdleTimer() @@ -63,8 +68,15 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN return true } + // handle favicon request + if isFaviconPath(r.URL.Path) { + r.URL.RawQuery = "alias=" + w.route.TargetName() + favicon.GetFavIcon(rw, r) + return false + } + // Check if start endpoint is configured and request path matches - if w.StartEndpoint != "" && r.URL.Path != w.StartEndpoint { + if w.Config().StartEndpoint != "" && r.URL.Path != w.Config().StartEndpoint { http.Error(rw, "Forbidden: Container can only be started via configured start endpoint", http.StatusForbidden) return false } @@ -88,7 +100,7 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN return false } - ctx, cancel := context.WithTimeoutCause(r.Context(), w.WakeTimeout, errors.New("wake timeout")) + ctx, cancel := context.WithTimeoutCause(r.Context(), w.Config().WakeTimeout, errors.New("wake timeout")) defer cancel() if w.cancelled(ctx, rw) { diff --git a/internal/docker/idlewatcher/waker_stream.go b/internal/docker/idlewatcher/waker_stream.go index 9c7954e9..22f55d32 100644 --- a/internal/docker/idlewatcher/waker_stream.go +++ b/internal/docker/idlewatcher/waker_stream.go @@ -61,7 +61,7 @@ func (w *Watcher) wakeFromStream() error { return wakeErr } - ctx, cancel := context.WithTimeoutCause(w.task.Context(), w.WakeTimeout, errors.New("wake timeout")) + ctx, cancel := context.WithTimeoutCause(w.task.Context(), w.Config().WakeTimeout, errors.New("wake timeout")) defer cancel() for { diff --git a/internal/docker/idlewatcher/watcher.go b/internal/docker/idlewatcher/watcher.go index 0f12a038..a504153a 100644 --- a/internal/docker/idlewatcher/watcher.go +++ b/internal/docker/idlewatcher/watcher.go @@ -26,8 +26,8 @@ type ( zerolog.Logger *waker - *containerMeta - *idlewatcher.Config + + route route.Route client *docker.SharedClient state atomic.Value[*containerState] @@ -52,11 +52,6 @@ const dockerReqTimeout = 3 * time.Second func registerWatcher(parent task.Parent, route route.Route, waker *waker) (*Watcher, error) { cfg := route.IdlewatcherConfig() - - if cfg.IdleTimeout == 0 { - panic(errShouldNotReachHere) - } - cont := route.ContainerInfo() key := cont.ContainerID @@ -79,11 +74,7 @@ func registerWatcher(parent task.Parent, route route.Route, waker *waker) (*Watc // FIXME: possible race condition here w.waker = waker - w.containerMeta = &containerMeta{ - ContainerID: cont.ContainerID, - ContainerName: cont.ContainerName, - } - w.Config = cfg + w.route = route w.ticker.Reset(cfg.IdleTimeout) if cont.Running { @@ -112,6 +103,10 @@ func registerWatcher(parent task.Parent, route route.Route, waker *waker) (*Watc return w, nil } +func (w *Watcher) Config() *idlewatcher.Config { + return w.route.IdlewatcherConfig() +} + func (w *Watcher) Wake() error { return w.wakeIfStopped() } @@ -141,7 +136,7 @@ func (w *Watcher) wakeIfStopped() error { return err } - ctx, cancel := context.WithTimeout(w.task.Context(), w.WakeTimeout) + ctx, cancel := context.WithTimeout(w.task.Context(), w.Config().WakeTimeout) defer cancel() // !Hard coded here since theres no constants from Docker API @@ -159,7 +154,7 @@ func (w *Watcher) wakeIfStopped() error { func (w *Watcher) getStopCallback() StopCallback { var cb func(context.Context) error - switch w.StopMethod { + switch w.Config().StopMethod { case idlewatcher.StopMethodPause: cb = w.containerPause case idlewatcher.StopMethodStop: @@ -170,7 +165,7 @@ func (w *Watcher) getStopCallback() StopCallback { panic(errShouldNotReachHere) } return func() error { - ctx, cancel := context.WithTimeout(w.task.Context(), time.Duration(w.StopTimeout)*time.Second) + ctx, cancel := context.WithTimeout(w.task.Context(), time.Duration(w.Config().StopTimeout)*time.Second) defer cancel() return cb(ctx) } @@ -178,19 +173,19 @@ func (w *Watcher) getStopCallback() StopCallback { func (w *Watcher) resetIdleTimer() { w.Trace().Msg("reset idle timer") - w.ticker.Reset(w.IdleTimeout) + w.ticker.Reset(w.Config().IdleTimeout) w.lastReset = time.Now() } func (w *Watcher) expires() time.Time { - return w.lastReset.Add(w.IdleTimeout) + return w.lastReset.Add(w.Config().IdleTimeout) } func (w *Watcher) getEventCh(ctx context.Context, dockerWatcher *watcher.DockerWatcher) (eventCh <-chan events.Event, errCh <-chan gperr.Error) { eventCh, errCh = dockerWatcher.EventsWithOptions(ctx, watcher.DockerListOptions{ Filters: watcher.NewDockerFilter( watcher.DockerFilterContainer, - watcher.DockerFilterContainerNameID(w.ContainerID), + watcher.DockerFilterContainerNameID(w.route.ContainerInfo().ContainerID), watcher.DockerFilterStart, watcher.DockerFilterStop, watcher.DockerFilterDie, @@ -249,20 +244,20 @@ func (w *Watcher) watchUntilDestroy() (returnCause error) { w.Error().Msg("unexpected docker event: " + e.String()) } // container name changed should also change the container id - if w.ContainerName != e.ActorName { - w.Debug().Msgf("renamed %s -> %s", w.ContainerName, e.ActorName) - w.ContainerName = e.ActorName - } - if w.ContainerID != e.ActorID { - w.Debug().Msgf("id changed %s -> %s", w.ContainerID, e.ActorID) - w.ContainerID = e.ActorID - // recreate event stream - eventCancel() + // if w.ContainerName != e.ActorName { + // w.Debug().Msgf("renamed %s -> %s", w.ContainerName, e.ActorName) + // w.ContainerName = e.ActorName + // } + // if w.ContainerID != e.ActorID { + // w.Debug().Msgf("id changed %s -> %s", w.ContainerID, e.ActorID) + // w.ContainerID = e.ActorID + // // recreate event stream + // eventCancel() - eventCtx, eventCancel = context.WithCancel(w.task.Context()) - defer eventCancel() - dockerEventCh, dockerEventErrCh = w.getEventCh(eventCtx, dockerWatcher) - } + // eventCtx, eventCancel = context.WithCancel(w.task.Context()) + // defer eventCancel() + // dockerEventCh, dockerEventErrCh = w.getEventCh(eventCtx, dockerWatcher) + // } case <-w.ticker.C: w.ticker.Stop() if w.running() { @@ -274,7 +269,7 @@ func (w *Watcher) watchUntilDestroy() (returnCause error) { if errors.Is(err, context.DeadlineExceeded) { err = errors.New("timeout waiting for container to stop, please set a higher value for `stop_timeout`") } - w.Err(err).Msgf("container stop with method %q failed", w.StopMethod) + w.Err(err).Msgf("container stop with method %q failed", w.Config().StopMethod) default: w.Info().Str("reason", "idle timeout").Msg("container stopped") }