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  }