Skip to content

Commit

Permalink
fix up livereload
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewmueller committed Dec 6, 2017
1 parent ab6d3a6 commit 82291ae
Show file tree
Hide file tree
Showing 5 changed files with 1,507 additions and 30 deletions.
5 changes: 5 additions & 0 deletions _examples/hello/hello.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package main

func main() {
println("cool")
}
71 changes: 41 additions & 30 deletions api/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ import (
"net/http"
"os"
"path"
"strconv"
"sync"
"time"

"golang.org/x/sync/errgroup"

"github.com/apex/log"
"github.com/apex/log/handlers/text"
"github.com/jaschaephraim/lrserver"
"github.com/julienschmidt/httprouter"
"github.com/matthewmueller/joy/internal/compiler"
"github.com/matthewmueller/joy/internal/compiler/util"
"github.com/matthewmueller/joy/internal/livereload"
"github.com/radovskyb/watcher"
)

Expand All @@ -32,14 +35,19 @@ const html = `<!doctype html>
</head>
<body>
<script src="/bundle.js"></script>
<script src="http://localhost:35729/livereload.js"></script>
<script src="/livereload.js"></script>
</body>
</html>`

// Serve fn
func Serve(ctx context.Context, settings *ServeSettings) error {
log.SetHandler(text.New(os.Stderr))

gosrc, e := util.GoSourcePath()
if e != nil {
return e
}

// compile
c := compiler.New(&compiler.Params{
Development: settings.Development,
Expand All @@ -62,35 +70,15 @@ func Serve(ctx context.Context, settings *ServeSettings) error {

// Add dir to watcher
for _, p := range index.Paths() {
src, e := util.GoSourcePath()
if e != nil {
return e
}

p = path.Join(src, p)
p = path.Join(gosrc, p)
log.Debugf("watching: %s", p)
if e := w.Add(p); e != nil {
return e
}
// // add packages
// for _, pkg := range main.Packages {
// pkgpath := path.Join(gosrc, pkg.Path)
// if e := w.Add(pkgpath); e != nil {
// return e
// }
// }

// // add raw files
// for _, file := range main.Files {
// pkgpath := path.Join(gosrc, file.Name)
// if e := w.Add(pkgpath); e != nil {
// return e
// }
// }
}

// Create and start LiveReload server
lr := lrserver.New(lrserver.DefaultName, lrserver.DefaultPort)
go lr.ListenAndServe()
lr := livereload.New(settings.Port)

// TODO: multiple package support
bundle := scripts[0].Source()
Expand All @@ -101,6 +89,7 @@ func Serve(ctx context.Context, settings *ServeSettings) error {
for {
select {
case <-w.Event:
log.Debugf("file changed")
files, err := c.Compile(settings.Packages...)
if err != nil {
log.WithError(err).Errorf("error compiling joy")
Expand All @@ -109,22 +98,29 @@ func Serve(ctx context.Context, settings *ServeSettings) error {
bundleLock.Lock()
bundle = files[0].Source()
bundleLock.Unlock()
lr.Reload(bundle)
log.Debugf("reloading bundle.js")
lr.Reload("/bundle.js")
case err := <-w.Error:
log.WithError(err).Errorf("error while reloading")
case <-w.Closed:
return
case <-ctx.Done():
w.Close()
return
}
}
}()

go w.Start(100 * time.Millisecond)

router := httprouter.New()

router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, html)
})

// attach livereload handlers
router.Handler("GET", "/livereload.js", lr)
router.Handler("GET", "/livereload", lr)

router.GET("/favicon.ico", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
w.WriteHeader(200)
})
Expand All @@ -137,6 +133,21 @@ func Serve(ctx context.Context, settings *ServeSettings) error {
fmt.Fprintf(w, src)
})

log.Infof("listening on http://localhost:8080")
return http.ListenAndServe(":8080", router)
server := &http.Server{
Addr: ":" + strconv.Itoa(settings.Port),
Handler: router,
}

eg := &errgroup.Group{}
eg.Go(func() error {
log.Infof("listening on http://localhost:8080")
return server.ListenAndServe()
})

// start watching
go w.Start(50 * time.Millisecond)

<-ctx.Done()
server.Shutdown(context.Background())
return nil
}
170 changes: 170 additions & 0 deletions internal/livereload/connection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package livereload

import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"

"golang.org/x/sync/errgroup"

"github.com/apex/log"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
)

var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}

type cmd struct {
Command string `json:"command"`
Protocols []string `json:"protocols"`
}

var protocols = []string{
"http://livereload.com/protocols/official-7",
"http://livereload.com/protocols/official-8",
"http://livereload.com/protocols/official-9",
"http://livereload.com/protocols/2.x-origin-version-negotiation",
"http://livereload.com/protocols/2.x-remote-control",
}

// Upgrade the connection
func upgrade(ctx context.Context, w http.ResponseWriter, r *http.Request) (*conn, error) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return nil, err
}

eg, ctx := errgroup.WithContext(ctx)

conn := &conn{
conn: c,
actionc: make(chan func() (json.RawMessage, error)),
ctx: ctx,
eg: eg,
ready: false,
}

// read from the browser
eg.Go(func() error {
for {
msgType, reader, err := c.NextReader()
if err != nil {
return err
}

// Close if binary instead of text
if msgType == websocket.BinaryMessage {
return errors.New("text message required recieved a binary message")
}

// Close if it's not JSON
command := &cmd{}
err = json.NewDecoder(reader).Decode(command)
if err != nil {
return errors.Wrapf(err, "error decoding command")
}

// ignore empty commands
if command.Command == "" {
return fmt.Errorf("server command shouldn't be empty")
}

// ensure we're ready to go
if !conn.ready {
if valid := validateHello(command); !valid {
return fmt.Errorf("invalid handshake")
}
conn.ready = true
log.Debugf("ready!")
}
}
})

// write to the browser
eg.Go(func() error {
for {
select {
case fn := <-conn.actionc:
// if !conn.ready {
// return fmt.Errorf("bad handshake")
// }

buf, err := fn()
if err != nil {
return errors.Wrapf(err, "error running fn")
}

log.Debugf("sending: %s", buf)
c.SetWriteDeadline(time.Now().Add(5 * time.Second))
if err := c.WriteJSON(buf); err != nil {
return errors.Wrapf(err, "error writing JSON")
}
case <-ctx.Done():
return ctx.Err()
}
}
})

// kick us off
conn.actionc <- func() (json.RawMessage, error) {
return json.Marshal(struct {
Command string `json:"command"`
Protocols []string `json:"protocols"`
ServerName string `json:"serverName"`
}{
Command: "hello",
Protocols: protocols,
ServerName: "joy",
})
}

return conn, nil
}

// conn is a single websocket connection
type conn struct {
actionc chan func() (json.RawMessage, error)
conn *websocket.Conn
ctx context.Context
eg *errgroup.Group
ready bool
}

// Reload a file
func (c *conn) Reload(file string) {
c.actionc <- func() (json.RawMessage, error) {
return json.Marshal(struct {
Command string `json:"command,omitempty"`
Path string `json:"path,omitempty"`
LiveCSS bool `json:"live_css,omitempty"`
}{
Command: "reload",
Path: file,
LiveCSS: true,
})
}
}

// Wait until the connection is over
func (c *conn) Wait() error {
return c.eg.Wait()
}

// validate the client's handshake
func validateHello(cmd *cmd) bool {
if cmd.Command != "hello" {
return false
}
for _, c := range cmd.Protocols {
for _, s := range protocols {
if c == s {
return true
}
}
}
return false
}
Loading

0 comments on commit 82291ae

Please sign in to comment.