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