Skip to content

Commit

Permalink
feat: Merge upstream PR glanceapp#241 auto reload config (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
ralphocdol authored Oct 29, 2024
1 parent a2a83e8 commit 452d55d
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 31 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
/build
/playground
glance*.yml
docker-compose.yml
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ require (
gopkg.in/yaml.v3 v3.0.1
)

require golang.org/x/sys v0.25.0 // indirect

require (
github.com/PuerkitoBio/goquery v1.10.0 // 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
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -52,6 +58,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.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.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=
Expand Down
23 changes: 23 additions & 0 deletions internal/assets/templates/document.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,28 @@
</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 protocol = window.location.protocol === "https:" ? "wss" : "ws";
let ws = new WebSocket(protocol + "://" + 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>
93 changes: 77 additions & 16 deletions internal/glance/glance.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,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 {
Expand Down Expand Up @@ -186,7 +198,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)
Expand All @@ -211,7 +223,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)
Expand Down Expand Up @@ -255,7 +267,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)

Expand All @@ -281,21 +293,25 @@ 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()

// 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 /{$}", a.HandlePageRequest)
mux.HandleFunc("GET /{page}", a.HandlePageRequest)
router.HandleFunc("/{page}", a.HandlePageRequest).Methods("GET")

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("/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.HandleFunc(fmt.Sprintf("GET /static/%s/manifest.json", a.Config.Server.AssetsHash), a.HandleManifestRequest)
router.HandleFunc(fmt.Sprintf("GET /static/%s/manifest.json", a.Config.Server.AssetsHash), a.HandleManifestRequest)

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)),
)

Expand All @@ -308,20 +324,65 @@ 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
}

func (a *Application) HandleManifestRequest(w http.ResponseWriter, r *http.Request) {
manifest := map[string]interface{}{
"name": func() string {
Expand Down
94 changes: 79 additions & 15 deletions internal/glance/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ package glance
import (
"fmt"
"os"
"time"

"github.com/fsnotify/fsnotify"
)

var (
currentApp *Application
done chan bool
)

func Main() int {
Expand All @@ -13,34 +21,90 @@ func Main() int {
return 1
}

configFile, err := os.Open(options.ConfigPath)
if options.Intent == CliIntentServe {
err := startWatcherAndApp(options.ConfigPath, false)
if err != nil {
fmt.Println(err)
return 1
}
}

return 0
}

func startWatcherAndApp(configPath string, sendReload bool) 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 {
if err := currentApp.Stop(); err != nil {
fmt.Printf("failed to shutdown application: %v\n", err)
}
time.Sleep(1 * time.Second) // give it enough time to shutdown
}
startWatcherAndApp(configPath, true)
}
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, sendReload)
<-done

return nil
}

func restartApplication(configPath string, sendReload bool) {
configFile, err := os.Open(configPath)
if err != nil {
fmt.Printf("failed opening config file: %v\n", err)
return 1
return
}

config, err := NewConfigFromYml(configFile)
configFile.Close()

if err != nil {
fmt.Printf("failed parsing config file: %v\n", err)
return 1
return
}

if options.Intent == CliIntentServe {
app, err := NewApplication(config)
app, err := NewApplication(config)
if err != nil {
fmt.Printf("failed creating application: %v\n", err)
return
}

if err != nil {
fmt.Printf("failed creating application: %v\n", err)
return 1
}
currentApp = app

if err := app.Serve(); err != nil {
fmt.Printf("http server error: %v\n", err)
return 1
}
if sendReload {
wsBroadcast <- []byte("reload")
}

if err := app.Serve(); err != nil {
fmt.Printf("http server error: %v\n", err)
}

return 0
}

0 comments on commit 452d55d

Please sign in to comment.