mirror of
https://github.com/m1k1o/neko-rooms.git
synced 2026-11-03 07:01:35 -06:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74fe2d7eab | |||
| a844a9f9d2 | |||
| afa67ac537 | |||
| d7f7acc7f3 | |||
| 2f7aa7ed13 | |||
| bf83709880 | |||
| 77695d42a3 | |||
| 2ac5e83448 | |||
| 38e4cab3fe | |||
| b62ac1097c | |||
| 7d70f7ac9b | |||
| 4d2393c457 | |||
| ab8c6a53ec | |||
| a6169da135 | |||
| ffeef5e887 |
@@ -411,6 +411,9 @@ components:
|
||||
running:
|
||||
type: boolean
|
||||
example: true
|
||||
is_ready:
|
||||
type: boolean
|
||||
example: true
|
||||
status:
|
||||
type: string
|
||||
example: Up 2 seconds
|
||||
|
||||
@@ -153,6 +153,12 @@ export default new Vuex.Store({
|
||||
const res = await defaultApi.pullStop()
|
||||
return res.data
|
||||
},
|
||||
|
||||
async EVENTS_SSE(): Promise<EventSource> {
|
||||
return new EventSource(configuration.basePath + '/api/events?sse', {
|
||||
withCredentials: true,
|
||||
})
|
||||
},
|
||||
},
|
||||
modules: {
|
||||
}
|
||||
|
||||
@@ -83,9 +83,10 @@ export default class Home extends Vue {
|
||||
public dialog = false
|
||||
|
||||
public interval!: number
|
||||
public autoRefresh = 10
|
||||
public autoRefresh = 0
|
||||
public autoRefreshItems = [
|
||||
{ text: 'Off', value: 0 },
|
||||
{ text: 'Off', value: -1 },
|
||||
{ text: 'Live', value: 0 },
|
||||
{ text: '5s', value: 5 },
|
||||
{ text: '10s', value: 10 },
|
||||
{ text: '30s', value: 30 },
|
||||
@@ -127,8 +128,11 @@ export default class Home extends Vue {
|
||||
window.clearInterval(this.interval)
|
||||
}
|
||||
|
||||
if (this.autoRefresh) {
|
||||
if (this.autoRefresh > 0) {
|
||||
this.sseClose()
|
||||
this.interval = window.setInterval(this.LoadRooms, this.autoRefresh * 1000)
|
||||
} else if (this.autoRefresh == 0) {
|
||||
this.sseStart()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +152,62 @@ export default class Home extends Vue {
|
||||
}
|
||||
|
||||
beforeDestroy() {
|
||||
this.autoRefresh = 0
|
||||
this.autoRefresh = -1
|
||||
}
|
||||
|
||||
sse: EventSource | null = null
|
||||
sseIsOpen = false
|
||||
sseReconnBackoff = 1000
|
||||
sseReconnRetries = 0
|
||||
|
||||
async sseStart(reconnect = false) {
|
||||
if (!reconnect) {
|
||||
this.sseIsOpen = true
|
||||
}
|
||||
|
||||
if (this.sse) {
|
||||
this.sse.close()
|
||||
}
|
||||
|
||||
this.sse = await this.$store.dispatch('EVENTS_SSE') as EventSource
|
||||
this.sseReconnRetries++
|
||||
|
||||
this.sse.addEventListener('rooms', (e) => {
|
||||
console.log('SSE rooms', e)
|
||||
this.LoadRooms()
|
||||
})
|
||||
|
||||
this.sse.addEventListener('error', (e: Event) => {
|
||||
console.log('SSE error', e)
|
||||
|
||||
// if is closed, don't try to reconnect
|
||||
if (!this.sseIsOpen) return
|
||||
|
||||
const wait = this.sseReconnBackoff + 100 * this.sseReconnRetries
|
||||
|
||||
// try to reconnect
|
||||
setTimeout(() => {
|
||||
this.sseStart(true)
|
||||
}, wait)
|
||||
|
||||
console.log('SSE error, trying to reconnect in ' + wait + 'ms')
|
||||
})
|
||||
|
||||
this.sse.addEventListener('message', (e: Event) => {
|
||||
console.log('SSE message', e)
|
||||
})
|
||||
|
||||
this.sse.addEventListener('open', (e: Event) => {
|
||||
console.log('SSE opened', e)
|
||||
})
|
||||
}
|
||||
|
||||
sseClose() {
|
||||
if (this.sse) {
|
||||
this.sse.close()
|
||||
}
|
||||
this.sse = null
|
||||
this.sseIsOpen = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -11,7 +11,9 @@ module.exports = defineConfig({
|
||||
proxy: process.env.API_PROXY ? {
|
||||
'^/api': {
|
||||
target: process.env.API_PROXY,
|
||||
timeout: 0, // because of SSE
|
||||
},
|
||||
} : undefined,
|
||||
compress: false, // because of SSE
|
||||
}
|
||||
})
|
||||
|
||||
@@ -62,4 +62,10 @@ func (manager *ApiManagerCtx) Mount(r chi.Router) {
|
||||
})
|
||||
|
||||
r.Get("/docker-compose.yaml", manager.dockerCompose)
|
||||
|
||||
//
|
||||
// events
|
||||
//
|
||||
|
||||
r.Get("/events", manager.events)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (manager *ApiManagerCtx) events(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
|
||||
sse := r.URL.Query().Has("sse")
|
||||
if sse {
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "Connection does not support streaming", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var ping <-chan time.Time
|
||||
if !sse {
|
||||
// dummy channel, never ping
|
||||
ping = make(<-chan time.Time)
|
||||
} else {
|
||||
// ping every 1 minute
|
||||
ticker := time.NewTicker(time.Minute)
|
||||
defer ticker.Stop()
|
||||
ping = ticker.C
|
||||
}
|
||||
|
||||
// listen for room events
|
||||
events, errs := manager.rooms.Events(r.Context())
|
||||
for {
|
||||
select {
|
||||
case <-ping:
|
||||
fmt.Fprintf(w, ": ping\n\n")
|
||||
flusher.Flush()
|
||||
case _, ok := <-errs:
|
||||
if !ok {
|
||||
manager.logger.Debug().Msg("sse channel closed")
|
||||
}
|
||||
return
|
||||
case e := <-events:
|
||||
jsonData, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
manager.logger.Err(err).Msg("failed to marshal event")
|
||||
continue
|
||||
}
|
||||
|
||||
if sse {
|
||||
fmt.Fprintf(w, "event: rooms\n")
|
||||
fmt.Fprintf(w, "data: %s\n\n", jsonData)
|
||||
} else {
|
||||
fmt.Fprintf(w, "rooms\t%s\n", jsonData)
|
||||
}
|
||||
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ type Room struct {
|
||||
PathPrefix string
|
||||
Labels []string
|
||||
WaitEnabled bool
|
||||
StopTimeoutSec int
|
||||
|
||||
StorageEnabled bool
|
||||
StorageInternal string
|
||||
@@ -100,6 +101,11 @@ func (Room) Init(cmd *cobra.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("stop_timeout", 10, "timeout in seconds for stopping the room with SIGTERM, after that SIGKILL is used (0 to disable, -1 to wait forever)")
|
||||
if err := viper.BindPFlag("stop_timeout", cmd.PersistentFlags().Lookup("stop_timeout")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Data
|
||||
|
||||
cmd.PersistentFlags().Bool("storage.enabled", true, "whether storage is enabled, where peristent containers data will be stored")
|
||||
@@ -207,6 +213,7 @@ func (s *Room) Set() {
|
||||
s.PathPrefix = path.Join("/", path.Clean(viper.GetString("path_prefix")))
|
||||
s.Labels = viper.GetStringSlice("labels")
|
||||
s.WaitEnabled = viper.GetBool("wait_enabled")
|
||||
s.StopTimeoutSec = viper.GetInt("stop_timeout")
|
||||
|
||||
s.StorageEnabled = viper.GetBool("storage.enabled")
|
||||
s.StorageInternal = viper.GetString("storage.internal")
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
var policiesJson string
|
||||
|
||||
func Generate(policies types.BrowserPolicyContent) (string, error) {
|
||||
policiesTmpl := map[string]interface{}{}
|
||||
policiesTmpl := map[string]any{}
|
||||
if err := json.Unmarshal([]byte(policiesJson), &policiesTmpl); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -23,7 +23,7 @@ func Generate(policies types.BrowserPolicyContent) (string, error) {
|
||||
// Extensions
|
||||
//
|
||||
|
||||
ExtensionInstallForcelist := []interface{}{}
|
||||
ExtensionInstallForcelist := []any{}
|
||||
for _, e := range policies.Extensions {
|
||||
URL := e.URL
|
||||
if URL == "" {
|
||||
@@ -36,7 +36,7 @@ func Generate(policies types.BrowserPolicyContent) (string, error) {
|
||||
)
|
||||
}
|
||||
|
||||
ExtensionInstallAllowlist := []interface{}{}
|
||||
ExtensionInstallAllowlist := []any{}
|
||||
for _, e := range policies.Extensions {
|
||||
ExtensionInstallAllowlist = append(
|
||||
ExtensionInstallAllowlist,
|
||||
@@ -46,7 +46,7 @@ func Generate(policies types.BrowserPolicyContent) (string, error) {
|
||||
|
||||
policiesTmpl["ExtensionInstallForcelist"] = ExtensionInstallForcelist
|
||||
policiesTmpl["ExtensionInstallAllowlist"] = ExtensionInstallAllowlist
|
||||
policiesTmpl["ExtensionInstallBlocklist"] = []interface{}{"*"}
|
||||
policiesTmpl["ExtensionInstallBlocklist"] = []any{"*"}
|
||||
|
||||
//
|
||||
// Developer Tools
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
func Parse(policiesJson string) (*types.BrowserPolicyContent, error) {
|
||||
policies := types.BrowserPolicyContent{}
|
||||
|
||||
policiesTmpl := map[string]interface{}{}
|
||||
policiesTmpl := map[string]any{}
|
||||
if err := json.Unmarshal([]byte(policiesJson), &policiesTmpl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -23,7 +23,7 @@ func Parse(policiesJson string) (*types.BrowserPolicyContent, error) {
|
||||
|
||||
if extensions, ok := policiesTmpl["ExtensionInstallForcelist"]; ok {
|
||||
policies.Extensions = []types.BrowserPolicyExtension{}
|
||||
for _, val := range extensions.([]interface{}) {
|
||||
for _, val := range extensions.([]any) {
|
||||
s := strings.Split(val.(string), ";")
|
||||
url := ""
|
||||
if len(s) > 1 {
|
||||
|
||||
@@ -14,7 +14,7 @@ var policiesJson string
|
||||
|
||||
func Generate(policies types.BrowserPolicyContent) (string, error) {
|
||||
policiesTmpl := struct {
|
||||
Policies map[string]interface{} `json:"policies"`
|
||||
Policies map[string]any `json:"policies"`
|
||||
}{}
|
||||
if err := json.Unmarshal([]byte(policiesJson), &policiesTmpl); err != nil {
|
||||
return "", err
|
||||
@@ -24,13 +24,13 @@ func Generate(policies types.BrowserPolicyContent) (string, error) {
|
||||
// Extensions
|
||||
//
|
||||
|
||||
ExtensionSettings := map[string]interface{}{}
|
||||
ExtensionSettings["*"] = map[string]interface{}{
|
||||
ExtensionSettings := map[string]any{}
|
||||
ExtensionSettings["*"] = map[string]any{
|
||||
"installation_mode": "blocked",
|
||||
}
|
||||
|
||||
for _, e := range policies.Extensions {
|
||||
ExtensionSettings[e.ID] = map[string]interface{}{
|
||||
ExtensionSettings[e.ID] = map[string]any{
|
||||
"install_url": e.URL,
|
||||
"installation_mode": "force_installed",
|
||||
}
|
||||
@@ -48,18 +48,18 @@ func Generate(policies types.BrowserPolicyContent) (string, error) {
|
||||
// Persistent Data
|
||||
//
|
||||
|
||||
Preferences := policiesTmpl.Policies["Preferences"].(map[string]interface{})
|
||||
Preferences := policiesTmpl.Policies["Preferences"].(map[string]any)
|
||||
Preferences["browser.urlbar.suggest.history"] = policies.PersistentData
|
||||
Preferences["places.history.enabled"] = policies.PersistentData
|
||||
policiesTmpl.Policies["Preferences"] = Preferences
|
||||
policiesTmpl.Policies["SanitizeOnShutdown"] = !policies.PersistentData
|
||||
|
||||
if policies.PersistentData {
|
||||
policiesTmpl.Policies["Homepage"] = map[string]interface{}{
|
||||
policiesTmpl.Policies["Homepage"] = map[string]any{
|
||||
"StartPage": "previous-session",
|
||||
}
|
||||
} else {
|
||||
policiesTmpl.Policies["Homepage"] = map[string]interface{}{
|
||||
policiesTmpl.Policies["Homepage"] = map[string]any{
|
||||
"StartPage": "homepage",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ func Parse(policiesJson string) (*types.BrowserPolicyContent, error) {
|
||||
policies := types.BrowserPolicyContent{}
|
||||
|
||||
policiesTmpl := struct {
|
||||
Policies map[string]interface{} `json:"policies"`
|
||||
Policies map[string]any `json:"policies"`
|
||||
}{}
|
||||
if err := json.Unmarshal([]byte(policiesJson), &policiesTmpl); err != nil {
|
||||
return nil, err
|
||||
@@ -30,12 +30,12 @@ func Parse(policiesJson string) (*types.BrowserPolicyContent, error) {
|
||||
|
||||
if extensions, ok := policiesTmpl.Policies["ExtensionSettings"]; ok {
|
||||
policies.Extensions = []types.BrowserPolicyExtension{}
|
||||
for id, val := range extensions.(map[string]interface{}) {
|
||||
for id, val := range extensions.(map[string]any) {
|
||||
if id == "*" {
|
||||
continue
|
||||
}
|
||||
|
||||
data := val.(map[string]interface{})
|
||||
data := val.(map[string]any)
|
||||
url, _ := data["install_url"].(string)
|
||||
|
||||
policies.Extensions = append(
|
||||
|
||||
+11
-1
@@ -59,6 +59,8 @@ func RoomNotFound(w http.ResponseWriter, r *http.Request, waitEnabled bool) {
|
||||
|
||||
if waitEnabled {
|
||||
roomWait(w, r)
|
||||
} else {
|
||||
w.Write([]byte(`<meta http-equiv="refresh" content="60">`))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,10 +83,12 @@ func RoomNotRunning(w http.ResponseWriter, r *http.Request, waitEnabled bool) {
|
||||
|
||||
if waitEnabled {
|
||||
roomWait(w, r)
|
||||
} else {
|
||||
w.Write([]byte(`<meta http-equiv="refresh" content="10">`))
|
||||
}
|
||||
}
|
||||
|
||||
func RoomNotReady(w http.ResponseWriter, r *http.Request) {
|
||||
func RoomNotReady(w http.ResponseWriter, r *http.Request, waitEnabled bool) {
|
||||
utils.Swal2Response(w, `
|
||||
<meta http-equiv="refresh" content="2">
|
||||
|
||||
@@ -102,6 +106,12 @@ func RoomNotReady(w http.ResponseWriter, r *http.Request) {
|
||||
<button type="button" onclick="location = location" class="swal2-confirm swal2-styled" style="margin-top: 1.25em">Reload</button>
|
||||
</div>
|
||||
`)
|
||||
|
||||
if waitEnabled {
|
||||
roomWait(w, r)
|
||||
} else {
|
||||
w.Write([]byte(`<meta http-equiv="refresh" content="2">`))
|
||||
}
|
||||
}
|
||||
|
||||
func RoomReady(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
+68
-152
@@ -2,7 +2,6 @@ package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
@@ -11,19 +10,19 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/m1k1o/neko-rooms/internal/room"
|
||||
"github.com/m1k1o/neko-rooms/internal/types"
|
||||
"github.com/m1k1o/neko-rooms/pkg/prefix"
|
||||
)
|
||||
|
||||
type entry struct {
|
||||
id string
|
||||
running bool
|
||||
handler *httputil.ReverseProxy
|
||||
ready bool
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
type wait struct {
|
||||
@@ -41,20 +40,18 @@ type ProxyManagerCtx struct {
|
||||
waitChans map[string]*wait
|
||||
waitEnabled bool
|
||||
|
||||
client *dockerClient.Client
|
||||
instanceName string
|
||||
handlers prefix.Tree[*entry]
|
||||
rooms *room.RoomManagerCtx
|
||||
handlers prefix.Tree[*entry]
|
||||
}
|
||||
|
||||
func New(client *dockerClient.Client, instanceName string, waitEnabled bool) *ProxyManagerCtx {
|
||||
func New(rooms *room.RoomManagerCtx, waitEnabled bool) *ProxyManagerCtx {
|
||||
return &ProxyManagerCtx{
|
||||
logger: log.With().Str("module", "proxy").Logger(),
|
||||
waitChans: map[string]*wait{},
|
||||
|
||||
client: client,
|
||||
instanceName: instanceName,
|
||||
waitEnabled: waitEnabled,
|
||||
handlers: prefix.NewTree[*entry](),
|
||||
rooms: rooms,
|
||||
waitEnabled: waitEnabled,
|
||||
handlers: prefix.NewTree[*entry](),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,48 +64,32 @@ func (p *ProxyManagerCtx) Start() {
|
||||
p.logger.Err(err).Msg("unable to refresh containers")
|
||||
}
|
||||
|
||||
msgs, errs := p.client.Events(p.ctx, dockerTypes.EventsOptions{
|
||||
Filters: filters.NewArgs(
|
||||
filters.Arg("type", "container"),
|
||||
filters.Arg("label", fmt.Sprintf("m1k1o.neko_rooms.instance=%s", p.instanceName)),
|
||||
filters.Arg("event", "create"),
|
||||
filters.Arg("event", "start"),
|
||||
//filters.Arg("event", "health_status"),
|
||||
filters.Arg("event", "stop"),
|
||||
filters.Arg("event", "destroy"),
|
||||
),
|
||||
})
|
||||
msgs, errs := p.rooms.Events(p.ctx)
|
||||
|
||||
for {
|
||||
select {
|
||||
case err, ok := <-errs:
|
||||
if !ok {
|
||||
p.logger.Fatal().Msg("docker event error channel closed")
|
||||
return
|
||||
}
|
||||
|
||||
p.logger.Err(err).Msg("got docker event error")
|
||||
p.logger.Err(err).Msg("room event error")
|
||||
case msg, ok := <-msgs:
|
||||
if !ok {
|
||||
p.logger.Fatal().Msg("docker event channel closed")
|
||||
return
|
||||
}
|
||||
|
||||
enabled, path, port, ok := p.parseLabels(msg.Actor.Attributes)
|
||||
enabled, path, port, ok := p.parseLabels(msg.ContainerLabels)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
host := msg.ID[:12] + ":" + port
|
||||
host := msg.ID + ":" + port
|
||||
|
||||
p.logger.Info().
|
||||
Str("action", msg.Action).
|
||||
Str("action", string(msg.Action)).
|
||||
Str("path", path).
|
||||
Str("host", host).
|
||||
Msg("got docker event")
|
||||
Msg("got room event")
|
||||
|
||||
// terminate waiting for any events
|
||||
if p.waitEnabled {
|
||||
// terminate waiting for room ready event
|
||||
if p.waitEnabled && msg.Action == types.RoomEventReady {
|
||||
p.waitMu.Lock()
|
||||
ch, ok := p.waitChans[path]
|
||||
if ok {
|
||||
@@ -120,32 +101,36 @@ func (p *ProxyManagerCtx) Start() {
|
||||
|
||||
p.mu.Lock()
|
||||
switch msg.Action {
|
||||
case "create":
|
||||
case types.RoomEventCreated:
|
||||
p.handlers.Insert(path, &entry{
|
||||
id: msg.ID,
|
||||
running: false,
|
||||
})
|
||||
case "start":
|
||||
case types.RoomEventStarted:
|
||||
p.handlers.Insert(path, &entry{
|
||||
id: msg.ID,
|
||||
running: true,
|
||||
ready: false,
|
||||
})
|
||||
case types.RoomEventReady:
|
||||
e := &entry{
|
||||
id: msg.ID,
|
||||
running: true,
|
||||
ready: true,
|
||||
}
|
||||
|
||||
// if proxying is disabled
|
||||
if enabled {
|
||||
e.handler = httputil.NewSingleHostReverseProxy(&url.URL{
|
||||
Scheme: "http",
|
||||
Host: host,
|
||||
})
|
||||
e.handler = p.newProxyHandler(path, host)
|
||||
}
|
||||
|
||||
p.handlers.Insert(path, e)
|
||||
case "stop":
|
||||
case types.RoomEventStopped:
|
||||
p.handlers.Insert(path, &entry{
|
||||
id: msg.ID,
|
||||
running: false,
|
||||
})
|
||||
case "destroy":
|
||||
case types.RoomEventDestroyed:
|
||||
p.handlers.Remove(path)
|
||||
}
|
||||
p.mu.Unlock()
|
||||
@@ -163,41 +148,30 @@ func (p *ProxyManagerCtx) Refresh() error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
containers, err := p.client.ContainerList(p.ctx, dockerTypes.ContainerListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(
|
||||
filters.Arg("label", fmt.Sprintf("m1k1o.neko_rooms.instance=%s", p.instanceName)),
|
||||
),
|
||||
})
|
||||
|
||||
rooms, err := p.rooms.List(p.ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.handlers = prefix.NewTree[*entry]()
|
||||
|
||||
for _, cont := range containers {
|
||||
enabled, path, port, ok := p.parseLabels(cont.Labels)
|
||||
for _, room := range rooms {
|
||||
enabled, path, port, ok := p.parseLabels(room.ContainerLabels)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
host := cont.ID[:12] + ":" + port
|
||||
host := room.ID + ":" + port
|
||||
|
||||
entry := &entry{
|
||||
id: cont.ID,
|
||||
id: room.ID,
|
||||
running: room.Running,
|
||||
ready: room.IsReady,
|
||||
}
|
||||
|
||||
if cont.State == "running" {
|
||||
entry.running = true
|
||||
|
||||
// if proxying is disabled
|
||||
if enabled {
|
||||
entry.handler = httputil.NewSingleHostReverseProxy(&url.URL{
|
||||
Scheme: "http",
|
||||
Host: host,
|
||||
})
|
||||
}
|
||||
// if proxying is enabled and room is ready
|
||||
if enabled && room.IsReady {
|
||||
entry.handler = p.newProxyHandler(path, host)
|
||||
}
|
||||
|
||||
p.handlers.Insert(path, entry)
|
||||
@@ -207,41 +181,9 @@ func (p *ProxyManagerCtx) Refresh() error {
|
||||
}
|
||||
|
||||
func (p *ProxyManagerCtx) parseLabels(labels map[string]string) (enabled bool, path string, port string, ok bool) {
|
||||
enabledStr, found := labels["m1k1o.neko_rooms.proxy.enabled"]
|
||||
if !found {
|
||||
//
|
||||
// workaround for legacy traefik labels
|
||||
//
|
||||
|
||||
// get room name
|
||||
var roomName string
|
||||
roomName, ok = labels["m1k1o.neko_rooms.name"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// get container name
|
||||
containerName := p.instanceName + "-" + roomName
|
||||
|
||||
// get path
|
||||
path, ok = labels["traefik.http.middlewares."+containerName+"-prf.stripprefix.prefixes"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// remove last /
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
|
||||
// get port
|
||||
port, ok = labels["traefik.http.services."+containerName+"-frontend.loadbalancer.server.port"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
//
|
||||
// workaround for legacy traefik labels
|
||||
//
|
||||
|
||||
var enabledStr string
|
||||
enabledStr, ok = labels["m1k1o.neko_rooms.proxy.enabled"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -257,6 +199,18 @@ func (p *ProxyManagerCtx) parseLabels(labels map[string]string) (enabled bool, p
|
||||
return
|
||||
}
|
||||
|
||||
func (p *ProxyManagerCtx) newProxyHandler(prefix, host string) http.Handler {
|
||||
handler := httputil.NewSingleHostReverseProxy(&url.URL{
|
||||
Scheme: "http",
|
||||
Host: host,
|
||||
})
|
||||
handler.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
p.logger.Err(err).Str("prefix", prefix).Msg("proxy error")
|
||||
http.Error(w, "unable to connect to room", http.StatusBadGateway)
|
||||
}
|
||||
return http.StripPrefix(prefix, handler)
|
||||
}
|
||||
|
||||
func (p *ProxyManagerCtx) waitForPath(w http.ResponseWriter, r *http.Request, path string) {
|
||||
p.logger.Debug().Str("path", path).Msg("adding new wait handler")
|
||||
|
||||
@@ -299,15 +253,21 @@ func (p *ProxyManagerCtx) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
proxy, prefix, ok := p.handlers.Match(cleanPath)
|
||||
p.mu.RUnlock()
|
||||
|
||||
// if room not found
|
||||
if !ok {
|
||||
// blocking until room is created
|
||||
// if room is not ready
|
||||
if !ok || !proxy.running || !proxy.ready {
|
||||
// blocking until room is ready
|
||||
if r.URL.Query().Has("wait") && p.waitEnabled {
|
||||
p.waitForPath(w, r, cleanPath)
|
||||
return
|
||||
}
|
||||
|
||||
RoomNotFound(w, r, p.waitEnabled)
|
||||
if !ok {
|
||||
RoomNotFound(w, r, p.waitEnabled)
|
||||
} else if !proxy.running {
|
||||
RoomNotRunning(w, r, p.waitEnabled)
|
||||
} else {
|
||||
RoomNotReady(w, r, p.waitEnabled)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -318,56 +278,12 @@ func (p *ProxyManagerCtx) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// if room not running
|
||||
if !proxy.running {
|
||||
// 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
|
||||
}
|
||||
|
||||
// if not proxying, check for container readiness status
|
||||
// if not proxying, just return room ready
|
||||
if proxy.handler == nil {
|
||||
containers, err := p.client.ContainerList(p.ctx, dockerTypes.ContainerListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(
|
||||
filters.Arg("label", fmt.Sprintf("m1k1o.neko_rooms.instance=%s", p.instanceName)),
|
||||
filters.Arg("id", proxy.id),
|
||||
),
|
||||
})
|
||||
|
||||
if err != nil || len(containers) != 1 {
|
||||
p.logger.Err(err).Msg("error while getting container ready status")
|
||||
RoomNotReady(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
container := containers[0]
|
||||
if strings.Contains(container.Status, "starting") {
|
||||
RoomNotReady(w, r)
|
||||
} else {
|
||||
RoomReady(w, r)
|
||||
}
|
||||
|
||||
RoomReady(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// handle not ready room
|
||||
proxy.handler.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
if r.URL.Path == "/" {
|
||||
RoomNotReady(w, r)
|
||||
}
|
||||
|
||||
p.logger.Err(err).Str("prefix", prefix).Msg("proxying error")
|
||||
}
|
||||
|
||||
// strip prefix from proxy
|
||||
handler := http.StripPrefix(prefix, proxy.handler)
|
||||
|
||||
// handle by proxy
|
||||
handler.ServeHTTP(w, r)
|
||||
proxy.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
@@ -19,17 +20,22 @@ func (manager *RoomManagerCtx) containerToEntry(container dockerTypes.Container)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roomId := container.ID[:12]
|
||||
|
||||
entry := &types.RoomEntry{
|
||||
ID: container.ID[:12],
|
||||
ID: roomId,
|
||||
URL: labels.URL,
|
||||
Name: labels.Name,
|
||||
NekoImage: labels.NekoImage,
|
||||
IsOutdated: labels.NekoImage != container.Image,
|
||||
MaxConnections: labels.Epr.Max - labels.Epr.Min + 1,
|
||||
Running: container.State == "running",
|
||||
IsReady: manager.events.IsRoomReady(roomId) || strings.Contains(container.Status, "healthy"),
|
||||
Status: container.Status,
|
||||
Created: time.Unix(container.Created, 0),
|
||||
Labels: labels.UserDefined,
|
||||
|
||||
ContainerLabels: container.Labels,
|
||||
}
|
||||
|
||||
if labels.Mux {
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
package room
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/m1k1o/neko-rooms/internal/config"
|
||||
"github.com/m1k1o/neko-rooms/internal/types"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type roomReady struct {
|
||||
id string
|
||||
labels map[string]string
|
||||
}
|
||||
|
||||
type events struct {
|
||||
wg sync.WaitGroup
|
||||
|
||||
logger zerolog.Logger
|
||||
config *config.Room
|
||||
client *dockerClient.Client
|
||||
|
||||
roomsReadyCh chan roomReady
|
||||
roomsReadyMu sync.Mutex
|
||||
roomsReady map[string]struct{}
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
listeners []chan types.RoomEvent
|
||||
listenersMu sync.Mutex
|
||||
}
|
||||
|
||||
func newEvents(config *config.Room, client *dockerClient.Client) *events {
|
||||
return &events{
|
||||
logger: log.With().Str("module", "events").Logger(),
|
||||
config: config,
|
||||
client: client,
|
||||
|
||||
roomsReadyCh: make(chan roomReady),
|
||||
roomsReady: make(map[string]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *events) Start() {
|
||||
e.ctx, e.cancel = context.WithCancel(context.Background())
|
||||
|
||||
msgs, errs := e.client.Events(e.ctx, dockerTypes.EventsOptions{
|
||||
Filters: filters.NewArgs(
|
||||
filters.Arg("type", "container"),
|
||||
filters.Arg("label", fmt.Sprintf("m1k1o.neko_rooms.instance=%s", e.config.InstanceName)),
|
||||
filters.Arg("event", "create"),
|
||||
filters.Arg("event", "start"),
|
||||
filters.Arg("event", "health_status"),
|
||||
filters.Arg("event", "stop"),
|
||||
filters.Arg("event", "destroy"),
|
||||
),
|
||||
})
|
||||
|
||||
e.wg.Add(1)
|
||||
go func() {
|
||||
defer e.wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-e.ctx.Done():
|
||||
e.logger.Info().Msg("docker event context closed")
|
||||
return
|
||||
case err, ok := <-errs:
|
||||
if !ok {
|
||||
e.logger.Error().Msg("docker event error channel closed")
|
||||
return
|
||||
}
|
||||
|
||||
e.logger.Err(err).Msg("got docker event error")
|
||||
return
|
||||
case room := <-e.roomsReadyCh:
|
||||
// ignore if room was already ready
|
||||
if !e.setRoomReady(room.id) {
|
||||
continue
|
||||
}
|
||||
|
||||
e.broadcast(types.RoomEvent{
|
||||
ID: room.id,
|
||||
Action: types.RoomEventReady,
|
||||
|
||||
ContainerLabels: room.labels,
|
||||
})
|
||||
case msg := <-msgs:
|
||||
roomId := msg.Actor.ID[:12]
|
||||
labels := msg.Actor.Attributes
|
||||
|
||||
e.logger.Debug().
|
||||
Str("id", roomId).
|
||||
Str("action", msg.Action).
|
||||
Msg("got docker event")
|
||||
|
||||
var action types.RoomEventAction
|
||||
switch msg.Action {
|
||||
case "create":
|
||||
action = types.RoomEventCreated
|
||||
case "start":
|
||||
action = types.RoomEventStarted
|
||||
e.waitForRoomReady(roomId, labels)
|
||||
case "health_status: healthy":
|
||||
action = types.RoomEventReady
|
||||
// ignore if room was already ready
|
||||
if !e.setRoomReady(roomId) {
|
||||
continue
|
||||
}
|
||||
case "stop":
|
||||
action = types.RoomEventStopped
|
||||
e.setRoomNotReady(roomId)
|
||||
case "destroy":
|
||||
action = types.RoomEventDestroyed
|
||||
}
|
||||
|
||||
e.broadcast(types.RoomEvent{
|
||||
ID: roomId,
|
||||
Action: action,
|
||||
|
||||
ContainerLabels: labels,
|
||||
})
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (e *events) Shutdown() error {
|
||||
e.cancel()
|
||||
close(e.roomsReadyCh)
|
||||
e.wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// room ready
|
||||
//
|
||||
|
||||
func (e *events) waitForRoomReady(roomId string, labels map[string]string) {
|
||||
e.wg.Add(1)
|
||||
go func() {
|
||||
defer e.wg.Done()
|
||||
|
||||
// check if room is ready
|
||||
exec, err := e.client.ContainerExecCreate(e.ctx, roomId, dockerTypes.ExecConfig{
|
||||
AttachStdout: true,
|
||||
Cmd: []string{
|
||||
"/bin/bash", "-c",
|
||||
fmt.Sprintf(`for ((a=1; a<=5; a++)); do (echo > /dev/tcp/localhost/%d) >/dev/null && echo -n OK && exit; sleep 1; done; exit`, frontendPort),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
e.logger.Err(err).Msg("failed to create exec")
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := e.client.ContainerExecAttach(e.ctx, exec.ID, dockerTypes.ExecStartCheck{})
|
||||
if err != nil {
|
||||
e.logger.Err(err).Msg("failed to attach exec")
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
data, err := io.ReadAll(conn.Reader)
|
||||
if err != nil {
|
||||
e.logger.Err(err).Msg("failed to read exec")
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasSuffix(string(data), "OK") {
|
||||
e.logger.Debug().Str("id", roomId).Msg("room ready")
|
||||
e.roomsReadyCh <- roomReady{
|
||||
id: roomId,
|
||||
labels: labels,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
e.logger.Warn().Str("id", roomId).Str("data", string(data)).Msg("room not ready")
|
||||
}()
|
||||
}
|
||||
|
||||
func (e *events) setRoomReady(roomId string) bool {
|
||||
e.roomsReadyMu.Lock()
|
||||
defer e.roomsReadyMu.Unlock()
|
||||
|
||||
_, ok := e.roomsReady[roomId]
|
||||
e.roomsReady[roomId] = struct{}{}
|
||||
return !ok
|
||||
}
|
||||
|
||||
func (e *events) setRoomNotReady(roomId string) {
|
||||
e.roomsReadyMu.Lock()
|
||||
defer e.roomsReadyMu.Unlock()
|
||||
|
||||
delete(e.roomsReady, roomId)
|
||||
}
|
||||
|
||||
func (e *events) IsRoomReady(roomId string) bool {
|
||||
e.roomsReadyMu.Lock()
|
||||
defer e.roomsReadyMu.Unlock()
|
||||
|
||||
_, ok := e.roomsReady[roomId]
|
||||
return ok
|
||||
}
|
||||
|
||||
//
|
||||
// events
|
||||
//
|
||||
|
||||
func (e *events) broadcast(event types.RoomEvent) {
|
||||
e.listenersMu.Lock()
|
||||
for _, listener := range e.listeners {
|
||||
listener <- event
|
||||
}
|
||||
e.listenersMu.Unlock()
|
||||
}
|
||||
|
||||
func (e *events) Events(ctx context.Context) (<-chan types.RoomEvent, <-chan error) {
|
||||
messages := make(chan types.RoomEvent)
|
||||
errs := make(chan error, 1)
|
||||
|
||||
// add listener
|
||||
e.listenersMu.Lock()
|
||||
e.listeners = append(e.listeners, messages)
|
||||
e.listenersMu.Unlock()
|
||||
|
||||
e.wg.Add(1)
|
||||
go func() {
|
||||
defer e.wg.Done()
|
||||
defer close(errs)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
errs <- ctx.Err()
|
||||
case <-e.ctx.Done():
|
||||
errs <- fmt.Errorf("room events shutdown")
|
||||
return
|
||||
}
|
||||
|
||||
// remove listener
|
||||
e.listenersMu.Lock()
|
||||
for i, listener := range e.listeners {
|
||||
if listener == messages {
|
||||
e.listeners = append(e.listeners[:i], e.listeners[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
e.listenersMu.Unlock()
|
||||
}()
|
||||
|
||||
return messages, errs
|
||||
}
|
||||
@@ -48,6 +48,7 @@ func New(client *dockerClient.Client, config *config.Room) *RoomManagerCtx {
|
||||
logger: logger,
|
||||
config: config,
|
||||
client: client,
|
||||
events: newEvents(config, client),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +56,7 @@ type RoomManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
config *config.Room
|
||||
client *dockerClient.Client
|
||||
events *events
|
||||
}
|
||||
|
||||
func (manager *RoomManagerCtx) Config() types.RoomsConfig {
|
||||
@@ -722,7 +724,10 @@ func (manager *RoomManagerCtx) Remove(ctx context.Context, id string) error {
|
||||
}
|
||||
|
||||
// Stop the actual container
|
||||
err = manager.client.ContainerStop(ctx, id, container.StopOptions{})
|
||||
err = manager.client.ContainerStop(ctx, id, container.StopOptions{
|
||||
Signal: "SIGTERM",
|
||||
Timeout: &manager.config.StopTimeoutSec,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -996,7 +1001,10 @@ func (manager *RoomManagerCtx) Stop(ctx context.Context, id string) error {
|
||||
}
|
||||
|
||||
// Stop the actual container
|
||||
return manager.client.ContainerStop(ctx, id, container.StopOptions{})
|
||||
return manager.client.ContainerStop(ctx, id, container.StopOptions{
|
||||
Signal: "SIGTERM",
|
||||
Timeout: &manager.config.StopTimeoutSec,
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *RoomManagerCtx) Restart(ctx context.Context, id string) error {
|
||||
@@ -1006,5 +1014,22 @@ func (manager *RoomManagerCtx) Restart(ctx context.Context, id string) error {
|
||||
}
|
||||
|
||||
// Restart the actual container
|
||||
return manager.client.ContainerRestart(ctx, id, container.StopOptions{})
|
||||
return manager.client.ContainerRestart(ctx, id, container.StopOptions{
|
||||
Signal: "SIGTERM",
|
||||
Timeout: &manager.config.StopTimeoutSec,
|
||||
})
|
||||
}
|
||||
|
||||
// events
|
||||
|
||||
func (manager *RoomManagerCtx) EventsLoopStart() {
|
||||
manager.events.Start()
|
||||
}
|
||||
|
||||
func (manager *RoomManagerCtx) EventsLoopStop() error {
|
||||
return manager.events.Shutdown()
|
||||
}
|
||||
|
||||
func (manager *RoomManagerCtx) Events(ctx context.Context) (<-chan types.RoomEvent, <-chan error) {
|
||||
return manager.events.Events(ctx)
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
const pprofPath = "/debug/pprof/"
|
||||
|
||||
func withPProf(router *chi.Mux) {
|
||||
router.Route(pprofPath, func(r chi.Router) {
|
||||
r.Get("/", pprof.Index)
|
||||
|
||||
r.Get("/{action}", func(w http.ResponseWriter, r *http.Request) {
|
||||
action := chi.URLParam(r, "action")
|
||||
|
||||
switch action {
|
||||
case "cmdline":
|
||||
pprof.Cmdline(w, r)
|
||||
case "profile":
|
||||
pprof.Profile(w, r)
|
||||
case "symbol":
|
||||
pprof.Symbol(w, r)
|
||||
case "trace":
|
||||
pprof.Trace(w, r)
|
||||
default:
|
||||
pprof.Handler(action).ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -14,7 +14,7 @@ type logformatter struct {
|
||||
}
|
||||
|
||||
func (l *logformatter) NewLogEntry(r *http.Request) middleware.LogEntry {
|
||||
req := map[string]interface{}{}
|
||||
req := map[string]any{}
|
||||
|
||||
if reqID := middleware.GetReqID(r.Context()); reqID != "" {
|
||||
req["id"] = reqID
|
||||
@@ -32,7 +32,7 @@ func (l *logformatter) NewLogEntry(r *http.Request) middleware.LogEntry {
|
||||
req["agent"] = r.UserAgent()
|
||||
req["uri"] = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI)
|
||||
|
||||
fields := map[string]interface{}{}
|
||||
fields := map[string]any{}
|
||||
fields["req"] = req
|
||||
|
||||
return &logentry{
|
||||
@@ -43,12 +43,12 @@ func (l *logformatter) NewLogEntry(r *http.Request) middleware.LogEntry {
|
||||
|
||||
type logentry struct {
|
||||
logger zerolog.Logger
|
||||
fields map[string]interface{}
|
||||
errors []map[string]interface{}
|
||||
fields map[string]any
|
||||
errors []map[string]any
|
||||
}
|
||||
|
||||
func (e *logentry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) {
|
||||
res := map[string]interface{}{}
|
||||
func (e *logentry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra any) {
|
||||
res := map[string]any{}
|
||||
res["time"] = time.Now().UTC().Format(time.RFC1123)
|
||||
res["status"] = status
|
||||
res["bytes"] = bytes
|
||||
@@ -65,8 +65,8 @@ func (e *logentry) Write(status, bytes int, header http.Header, elapsed time.Dur
|
||||
}
|
||||
}
|
||||
|
||||
func (e *logentry) Panic(v interface{}, stack []byte) {
|
||||
err := map[string]interface{}{}
|
||||
func (e *logentry) Panic(v any, stack []byte) {
|
||||
err := map[string]any{}
|
||||
err["message"] = fmt.Sprintf("%+v", v)
|
||||
err["stack"] = string(stack)
|
||||
|
||||
|
||||
@@ -58,12 +58,6 @@ func New(ApiManager types.ApiManager, roomConfig *config.Room, config *config.Se
|
||||
}))
|
||||
}
|
||||
|
||||
// mount pprof endpoint
|
||||
if config.PProf {
|
||||
withPProf(router)
|
||||
logger.Info().Msgf("with pprof endpoint at %s", pprofPath)
|
||||
}
|
||||
|
||||
//
|
||||
// admin page
|
||||
//
|
||||
@@ -240,6 +234,12 @@ func New(ApiManager types.ApiManager, roomConfig *config.Room, config *config.Se
|
||||
})
|
||||
}
|
||||
|
||||
// mount pprof endpoint
|
||||
if config.PProf {
|
||||
router.Mount("/debug", middleware.Profiler())
|
||||
logger.Info().Msgf("with pprof endpoint")
|
||||
}
|
||||
|
||||
// handle all remaining paths with proxy
|
||||
router.Handle("/*", proxyHandler)
|
||||
|
||||
|
||||
@@ -23,9 +23,12 @@ type RoomEntry struct {
|
||||
IsOutdated bool `json:"is_outdated"`
|
||||
MaxConnections uint16 `json:"max_connections"` // 0 when using mux
|
||||
Running bool `json:"running"`
|
||||
IsReady bool `json:"is_ready"`
|
||||
Status string `json:"status"`
|
||||
Created time.Time `json:"created"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
|
||||
ContainerLabels map[string]string `json:"-"` // for internal use
|
||||
}
|
||||
|
||||
type MountType string
|
||||
@@ -138,6 +141,23 @@ type RoomMember struct {
|
||||
Muted bool `json:"muted"`
|
||||
}
|
||||
|
||||
type RoomEventAction string
|
||||
|
||||
const (
|
||||
RoomEventCreated RoomEventAction = "created"
|
||||
RoomEventStarted RoomEventAction = "started"
|
||||
RoomEventReady RoomEventAction = "ready"
|
||||
RoomEventStopped RoomEventAction = "stopped"
|
||||
RoomEventDestroyed RoomEventAction = "destroyed"
|
||||
)
|
||||
|
||||
type RoomEvent struct {
|
||||
ID string `json:"id"`
|
||||
Action RoomEventAction `json:"action"`
|
||||
|
||||
ContainerLabels map[string]string `json:"-"` // for internal use
|
||||
}
|
||||
|
||||
var ErrRoomNotFound = fmt.Errorf("room not found")
|
||||
|
||||
type RoomManager interface {
|
||||
@@ -155,4 +175,8 @@ type RoomManager interface {
|
||||
Start(ctx context.Context, id string) error
|
||||
Stop(ctx context.Context, id string) error
|
||||
Restart(ctx context.Context, id string) error
|
||||
|
||||
EventsLoopStart()
|
||||
EventsLoopStop() error
|
||||
Events(ctx context.Context) (<-chan RoomEvent, <-chan error)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,6 @@ func Color(str string) string {
|
||||
return result + str[lastIndex:]
|
||||
}
|
||||
|
||||
func Colorf(format string, a ...interface{}) string {
|
||||
func Colorf(format string, a ...any) string {
|
||||
return fmt.Sprintf(Color(format), a...)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ func Swal2Response(w http.ResponseWriter, body string) {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
|
||||
err = tmpl.Execute(w, map[string]interface{}{
|
||||
err = tmpl.Execute(w, map[string]any{
|
||||
"Body": template.HTML(body),
|
||||
})
|
||||
|
||||
|
||||
@@ -131,6 +131,7 @@ func (main *MainCtx) Start() {
|
||||
client,
|
||||
main.Configs.Room,
|
||||
)
|
||||
main.roomManager.EventsLoopStart()
|
||||
|
||||
main.pullManager = pull.New(
|
||||
client,
|
||||
@@ -143,8 +144,7 @@ func (main *MainCtx) Start() {
|
||||
)
|
||||
|
||||
main.proxyManager = proxy.New(
|
||||
client,
|
||||
main.Configs.Room.InstanceName,
|
||||
main.roomManager,
|
||||
main.Configs.Room.WaitEnabled,
|
||||
)
|
||||
main.proxyManager.Start()
|
||||
@@ -169,6 +169,9 @@ func (main *MainCtx) Shutdown() {
|
||||
|
||||
err = main.pullManager.Shutdown()
|
||||
main.logger.Err(err).Msg("pull manager shutdown")
|
||||
|
||||
err = main.roomManager.EventsLoopStop()
|
||||
main.logger.Err(err).Msg("room events loop shutdown")
|
||||
}
|
||||
|
||||
func (main *MainCtx) ServeCommand(cmd *cobra.Command, args []string) {
|
||||
|
||||
Reference in New Issue
Block a user