github.com/wader/devd@v0.0.0-20221031103345-441c7e455249/livereload/livereload.go (about) 1 // Package livereload allows HTML pages to be dynamically reloaded. It includes 2 // both the server and client implementations required. 3 package livereload 4 5 import ( 6 _ "embed" 7 "net/http" 8 "regexp" 9 "strings" 10 "sync" 11 12 "github.com/cortesi/termlog" 13 "github.com/gorilla/websocket" 14 "github.com/wader/devd/inject" 15 ) 16 17 //go:embed client.js 18 var clientJS []byte 19 20 //go:embed LICENSE 21 var license []byte 22 23 // Reloader triggers a reload 24 type Reloader interface { 25 Reload(paths []string) 26 Watch(ch chan []string) 27 } 28 29 const ( 30 cmdPage = "page" 31 cmdCSS = "css" 32 // EndpointPath is the path to the websocket endpoint 33 EndpointPath = "/.devd.livereload" 34 // ScriptPath is the path to the livereload JavaScript asset 35 ScriptPath = "/.devd.livereload.js" 36 ) 37 38 // Injector for the livereload script 39 var Injector = inject.CopyInject{ 40 Within: 1024 * 30, 41 ContentType: "text/html", 42 Marker: regexp.MustCompile(`<\/head>`), 43 Payload: []byte(`<script src="/.devd.livereload.js"></script>`), 44 } 45 46 // Server implements a Livereload server 47 type Server struct { 48 sync.Mutex 49 broadcast chan<- string 50 51 logger termlog.Logger 52 name string 53 connections map[*websocket.Conn]bool 54 } 55 56 // NewServer createss a Server instance 57 func NewServer(name string, logger termlog.Logger) *Server { 58 broadcast := make(chan string, 50) 59 s := &Server{ 60 name: name, 61 broadcast: broadcast, 62 connections: make(map[*websocket.Conn]bool), 63 logger: logger, 64 } 65 go s.run(broadcast) 66 return s 67 } 68 69 func (s *Server) run(broadcast <-chan string) { 70 for m := range broadcast { 71 s.Lock() 72 for conn := range s.connections { 73 if conn == nil { 74 continue 75 } 76 err := conn.WriteMessage(websocket.TextMessage, []byte(m)) 77 if err != nil { 78 s.logger.Say("Error: %s", err) 79 delete(s.connections, conn) 80 } 81 } 82 s.Unlock() 83 } 84 s.Lock() 85 defer s.Unlock() 86 for conn := range s.connections { 87 delete(s.connections, conn) 88 conn.Close() 89 } 90 } 91 92 var upgrader = websocket.Upgrader{ 93 ReadBufferSize: 1024, 94 WriteBufferSize: 1024, 95 CheckOrigin: func(r *http.Request) bool { return true }, 96 } 97 98 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 99 if r.Method != "GET" { 100 http.Error(w, "Method not allowed", 405) 101 return 102 } 103 conn, err := upgrader.Upgrade(w, r, nil) 104 if err != nil { 105 s.logger.Say("Error: %s", err) 106 http.Error(w, "Can't upgrade.", 500) 107 return 108 } 109 s.Lock() 110 s.connections[conn] = true 111 s.Unlock() 112 } 113 114 // Reload signals to connected clients that a given resource should be 115 // reloaded. 116 func (s *Server) Reload(paths []string) { 117 cmd := cmdCSS 118 for _, path := range paths { 119 if !strings.HasSuffix(path, ".css") { 120 cmd = cmdPage 121 } 122 } 123 s.logger.SayAs("debug", "livereload %s, files changed: %s", cmd, paths) 124 s.broadcast <- cmd 125 } 126 127 // Watch montors a channel of lists of paths for reload requests 128 func (s *Server) Watch(ch chan []string) { 129 for ei := range ch { 130 if len(ei) > 0 { 131 s.Reload(ei) 132 } 133 } 134 } 135 136 // ServeScript is a handler function that serves the livereload JavaScript file 137 func (s *Server) ServeScript(rw http.ResponseWriter, req *http.Request) { 138 rw.Header().Set("Content-Type", "application/javascript") 139 _, err := rw.Write(clientJS) 140 if err != nil { 141 s.logger.Warn("Error serving livereload script: %s", err) 142 } 143 }