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  }