diff --git a/internal/config/room.go b/internal/config/room.go index b65dbfc..9e302db 100644 --- a/internal/config/room.go +++ b/internal/config/room.go @@ -31,6 +31,7 @@ type Room struct { NekoPrivilegedImages []string PathPrefix string Labels []string + WaitEnabled bool StorageEnabled bool StorageInternal string @@ -93,6 +94,11 @@ func (Room) Init(cmd *cobra.Command) error { return err } + cmd.PersistentFlags().Bool("wait_enabled", true, "enable active waiting for the room") + if err := viper.BindPFlag("wait_enabled", cmd.PersistentFlags().Lookup("wait_enabled")); err != nil { + return err + } + // Data cmd.PersistentFlags().Bool("storage.enabled", true, "whether storage is enabled, where peristent containers data will be stored") @@ -199,6 +205,7 @@ func (s *Room) Set() { s.NekoPrivilegedImages = viper.GetStringSlice("neko_privileged_images") s.PathPrefix = path.Join("/", path.Clean(viper.GetString("path_prefix"))) s.Labels = viper.GetStringSlice("labels") + s.WaitEnabled = viper.GetBool("wait_enabled") s.StorageEnabled = viper.GetBool("storage.enabled") s.StorageInternal = viper.GetString("storage.internal") diff --git a/internal/proxy/lobby.go b/internal/proxy/lobby.go index fc81add..23c294f 100644 --- a/internal/proxy/lobby.go +++ b/internal/proxy/lobby.go @@ -6,7 +6,41 @@ import ( "github.com/m1k1o/neko-rooms/internal/utils" ) -func RoomNotFound(w http.ResponseWriter, r *http.Request) { +func roomWait(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(``)) +} + +func RoomNotFound(w http.ResponseWriter, r *http.Request, waitEnabled bool) { utils.Swal2Response(w, `
@@ -16,11 +50,19 @@ func RoomNotFound(w http.ResponseWriter, r *http.Request) {
The room you are trying to join does not exist.
+
You can wait on this page until it will be created.
+
+
+
`) + + if waitEnabled { + roomWait(w, r) + } } -func RoomNotRunning(w http.ResponseWriter, r *http.Request) { +func RoomNotRunning(w http.ResponseWriter, r *http.Request, waitEnabled bool) { utils.Swal2Response(w, `
@@ -30,8 +72,16 @@ func RoomNotRunning(w http.ResponseWriter, r *http.Request) {
The room you are trying to join is not running.
+
You can wait on this page until it will be started.
+
+
+
`) + + if waitEnabled { + roomWait(w, r) + } } func RoomNotReady(w http.ResponseWriter, r *http.Request) { diff --git a/internal/proxy/manager.go b/internal/proxy/manager.go index 4c73974..3890b02 100644 --- a/internal/proxy/manager.go +++ b/internal/proxy/manager.go @@ -6,6 +6,7 @@ import ( "net/http" "net/http/httputil" "net/url" + "path" "strconv" "strings" "sync" @@ -23,23 +24,34 @@ type entry struct { handler *httputil.ReverseProxy } +type wait struct { + subs int + signal chan struct{} +} + type ProxyManagerCtx struct { logger zerolog.Logger mu sync.RWMutex ctx context.Context cancel func() + waitMu sync.RWMutex + waitChans map[string]*wait + waitEnabled bool + client *dockerClient.Client instanceName string handlers *prefixHandler[*entry] } -func New(client *dockerClient.Client, instanceName string) *ProxyManagerCtx { +func New(client *dockerClient.Client, instanceName string, waitEnabled bool) *ProxyManagerCtx { return &ProxyManagerCtx{ - logger: log.With().Str("module", "proxy").Logger(), + logger: log.With().Str("module", "proxy").Logger(), + waitChans: map[string]*wait{}, client: client, instanceName: instanceName, + waitEnabled: waitEnabled, handlers: &prefixHandler[*entry]{}, } } @@ -83,6 +95,17 @@ func (p *ProxyManagerCtx) Start() { Str("host", host). Msg("got docker event") + // terminate waiting for any events + if p.waitEnabled { + p.waitMu.Lock() + ch, ok := p.waitChans[path] + if ok { + close(ch.signal) + delete(p.waitChans, path) + } + p.waitMu.Unlock() + } + p.mu.Lock() switch msg.Action { case "create": @@ -222,28 +245,76 @@ func (p *ProxyManagerCtx) parseLabels(labels map[string]string) (enabled bool, p return } +func (p *ProxyManagerCtx) waitForPath(w http.ResponseWriter, r *http.Request, path string) { + p.logger.Debug().Str("path", path).Msg("adding new wait handler") + + p.waitMu.Lock() + ch, ok := p.waitChans[path] + if !ok { + ch = &wait{ + subs: 1, + signal: make(chan struct{}), + } + p.waitChans[path] = ch + } else { + p.waitChans[path].subs += 1 + } + p.waitMu.Unlock() + + select { + case <-ch.signal: + w.Write([]byte("ready")) + case <-r.Context().Done(): + http.Error(w, r.Context().Err().Error(), http.StatusRequestTimeout) + + p.waitMu.Lock() + ch.subs -= 1 + if ch.subs <= 0 { + delete(p.waitChans, path) + } + p.waitMu.Unlock() + p.logger.Debug().Str("path", path).Msg("wait handler removed") + case <-p.ctx.Done(): + w.Write([]byte("shutdown")) + } +} + func (p *ProxyManagerCtx) ServeHTTP(w http.ResponseWriter, r *http.Request) { + cleanPath := path.Clean(r.URL.Path) + // get proxy by room name p.mu.RLock() - proxy, prefix, ok := p.handlers.Match(r.URL.Path) + proxy, prefix, ok := p.handlers.Match(cleanPath) p.mu.RUnlock() // if room not found if !ok { - RoomNotFound(w, r) + // blocking until room is created + if r.URL.Query().Has("wait") && p.waitEnabled { + p.waitForPath(w, r, cleanPath) + return + } + + RoomNotFound(w, r, p.waitEnabled) return } // redirect to room ending with / - if r.URL.Path == prefix { - r.URL.Path += "/" + if cleanPath == prefix && !strings.HasSuffix(r.URL.Path, "/") { + r.URL.Path = cleanPath + "/" http.Redirect(w, r, r.URL.String(), http.StatusTemporaryRedirect) return } // if room not running if !proxy.running { - RoomNotRunning(w, r) + // blocking until room is running + if r.URL.Query().Has("wait") && p.waitEnabled { + p.waitForPath(w, r, cleanPath) + return + } + + RoomNotRunning(w, r, p.waitEnabled) return } diff --git a/neko.go b/neko.go index da8c7d6..9fb3822 100644 --- a/neko.go +++ b/neko.go @@ -145,6 +145,7 @@ func (main *MainCtx) Start() { main.proxyManager = proxy.New( client, main.Configs.Room.InstanceName, + main.Configs.Room.WaitEnabled, ) main.proxyManager.Start()