#237 reload on config change
This commit is contained in:
parent
d90d39933a
commit
46ad4253ac
6 changed files with 194 additions and 38 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@
|
|||
/build
|
||||
/playground
|
||||
glance*.yml
|
||||
docker-compose.yml
|
||||
|
|
5
go.mod
5
go.mod
|
@ -8,9 +8,14 @@ require (
|
|||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.22.0 // indirect
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.9.2 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mmcdole/goxpp v1.1.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
|
|
8
go.sum
8
go.sum
|
@ -5,7 +5,13 @@ github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4=
|
||||
|
@ -45,6 +51,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
|
|
|
@ -20,5 +20,27 @@
|
|||
</head>
|
||||
<body>
|
||||
{{ template "document-body" . }}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
function createWebSocket() {
|
||||
let host = window.location.hostname;
|
||||
let port = window.location.protocol === "https:" ? (window.location.port || 443) : (window.location.port || 80);
|
||||
let ws = new WebSocket("ws://" + host + ":" + port + "/ws");
|
||||
console.log("WebSocket connection established");
|
||||
ws.onmessage = function(event) {
|
||||
if (event.data === "reload") {
|
||||
location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = function() {
|
||||
console.log("WebSocket connection closed, attempting to reconnect...");
|
||||
setTimeout(createWebSocket, 1000);
|
||||
};
|
||||
}
|
||||
|
||||
createWebSocket();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -16,17 +16,29 @@ import (
|
|||
|
||||
"github.com/glanceapp/glance/internal/assets"
|
||||
"github.com/glanceapp/glance/internal/widget"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var buildVersion = "dev"
|
||||
|
||||
var sequentialWhitespacePattern = regexp.MustCompile(`\s+`)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
var wsClients = make(map[*websocket.Conn]bool)
|
||||
var wsBroadcast = make(chan []byte)
|
||||
|
||||
type Application struct {
|
||||
Version string
|
||||
Config Config
|
||||
slugToPage map[string]*Page
|
||||
widgetByID map[uint64]widget.Widget
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
type Theme struct {
|
||||
|
@ -173,7 +185,7 @@ func NewApplication(config *Config) (*Application, error) {
|
|||
}
|
||||
|
||||
func (a *Application) HandlePageRequest(w http.ResponseWriter, r *http.Request) {
|
||||
page, exists := a.slugToPage[r.PathValue("page")]
|
||||
page, exists := a.slugToPage[mux.Vars(r)["page"]]
|
||||
|
||||
if !exists {
|
||||
a.HandleNotFound(w, r)
|
||||
|
@ -198,7 +210,7 @@ func (a *Application) HandlePageRequest(w http.ResponseWriter, r *http.Request)
|
|||
}
|
||||
|
||||
func (a *Application) HandlePageContentRequest(w http.ResponseWriter, r *http.Request) {
|
||||
page, exists := a.slugToPage[r.PathValue("page")]
|
||||
page, exists := a.slugToPage[mux.Vars(r)["page"]]
|
||||
|
||||
if !exists {
|
||||
a.HandleNotFound(w, r)
|
||||
|
@ -242,7 +254,7 @@ func FileServerWithCache(fs http.FileSystem, cacheDuration time.Duration) http.H
|
|||
}
|
||||
|
||||
func (a *Application) HandleWidgetRequest(w http.ResponseWriter, r *http.Request) {
|
||||
widgetValue := r.PathValue("widget")
|
||||
widgetValue := mux.Vars(r)["widget"]
|
||||
|
||||
widgetID, err := strconv.ParseUint(widgetValue, 10, 64)
|
||||
|
||||
|
@ -268,19 +280,23 @@ func (a *Application) AssetPath(asset string) string {
|
|||
func (a *Application) Serve() error {
|
||||
// TODO: add gzip support, static files must have their gzipped contents cached
|
||||
// TODO: add HTTPS support
|
||||
mux := http.NewServeMux()
|
||||
router := mux.NewRouter()
|
||||
|
||||
mux.HandleFunc("GET /{$}", a.HandlePageRequest)
|
||||
mux.HandleFunc("GET /{page}", a.HandlePageRequest)
|
||||
// In gorilla/mux, routes are matched in the order they are registered,
|
||||
// so more specific routes should be registered before more general ones
|
||||
router.HandleFunc("/ws", a.handleWebSocket)
|
||||
|
||||
mux.HandleFunc("GET /api/pages/{page}/content/{$}", a.HandlePageContentRequest)
|
||||
mux.HandleFunc("/api/widgets/{widget}/{path...}", a.HandleWidgetRequest)
|
||||
mux.HandleFunc("GET /api/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
router.HandleFunc("/{page}", a.HandlePageRequest).Methods("GET")
|
||||
|
||||
router.HandleFunc("/api/pages/{page}/content/", a.HandlePageContentRequest).Methods("GET")
|
||||
router.HandleFunc("/api/widgets/{widget}/{path:.*}", a.HandleWidgetRequest)
|
||||
router.HandleFunc("/api/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
}).Methods("GET")
|
||||
router.HandleFunc("/", a.HandlePageRequest).Methods("GET")
|
||||
|
||||
mux.Handle(
|
||||
fmt.Sprintf("GET /static/%s/{path...}", a.Config.Server.AssetsHash),
|
||||
router.Handle(
|
||||
fmt.Sprintf("/static/%s/{path:.*}", a.Config.Server.AssetsHash),
|
||||
http.StripPrefix("/static/"+a.Config.Server.AssetsHash, FileServerWithCache(http.FS(assets.PublicFS), 24*time.Hour)),
|
||||
)
|
||||
|
||||
|
@ -293,16 +309,61 @@ func (a *Application) Serve() error {
|
|||
|
||||
slog.Info("Serving assets", "path", absAssetsPath)
|
||||
assetsFS := FileServerWithCache(http.Dir(a.Config.Server.AssetsPath), 2*time.Hour)
|
||||
mux.Handle("/assets/{path...}", http.StripPrefix("/assets/", assetsFS))
|
||||
router.Handle("/assets/{path:.*}", http.StripPrefix("/assets/", assetsFS))
|
||||
}
|
||||
|
||||
server := http.Server{
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", a.Config.Server.Host, a.Config.Server.Port),
|
||||
Handler: mux,
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
a.server = server
|
||||
|
||||
go a.handleWebSocketMessages()
|
||||
|
||||
a.Config.Server.StartedAt = time.Now()
|
||||
slog.Info("Starting server", "host", a.Config.Server.Host, "port", a.Config.Server.Port, "base-url", a.Config.Server.BaseURL)
|
||||
|
||||
return server.ListenAndServe()
|
||||
}
|
||||
|
||||
func (a *Application) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to upgrade to websocket: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
wsClients[conn] = true
|
||||
|
||||
for {
|
||||
_, _, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
delete(wsClients, conn)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Application) handleWebSocketMessages() {
|
||||
for {
|
||||
msg := <-wsBroadcast
|
||||
for client := range wsClients {
|
||||
err := client.WriteMessage(websocket.TextMessage, msg)
|
||||
if err != nil {
|
||||
client.Close()
|
||||
delete(wsClients, client)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Application) Stop() error {
|
||||
if a.server != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
return a.server.Shutdown(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -3,6 +3,13 @@ package glance
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
var (
|
||||
currentApp *Application
|
||||
done chan bool
|
||||
)
|
||||
|
||||
func Main() int {
|
||||
|
@ -13,34 +20,86 @@ func Main() int {
|
|||
return 1
|
||||
}
|
||||
|
||||
configFile, err := os.Open(options.ConfigPath)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("failed opening config file: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
config, err := NewConfigFromYml(configFile)
|
||||
configFile.Close()
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("failed parsing config file: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
if options.Intent == CliIntentServe {
|
||||
app, err := NewApplication(config)
|
||||
|
||||
err := startWatcherAndApp(options.ConfigPath)
|
||||
if err != nil {
|
||||
fmt.Printf("failed creating application: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
if err := app.Serve(); err != nil {
|
||||
fmt.Printf("http server error: %v\n", err)
|
||||
fmt.Println(err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func startWatcherAndApp(configPath string) error {
|
||||
done = make(chan bool)
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file watcher: %v", err)
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||
fmt.Println("config file modified, restarting application...")
|
||||
if currentApp != nil {
|
||||
wsBroadcast <- []byte("reload")
|
||||
if err := currentApp.Stop(); err != nil {
|
||||
fmt.Printf("failed to shutdown application: %v\n", err)
|
||||
}
|
||||
}
|
||||
startWatcherAndApp(configPath)
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
fmt.Printf("error watching config file: %v\n", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err = watcher.Add(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to watch config file: %v", err)
|
||||
}
|
||||
|
||||
restartApplication(configPath)
|
||||
<-done
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func restartApplication(configPath string) {
|
||||
|
||||
configFile, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
fmt.Printf("failed opening config file: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
config, err := NewConfigFromYml(configFile)
|
||||
configFile.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("failed parsing config file: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
app, err := NewApplication(config)
|
||||
if err != nil {
|
||||
fmt.Printf("failed creating application: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
currentApp = app
|
||||
|
||||
if err := app.Serve(); err != nil {
|
||||
fmt.Printf("http server error: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue