Skip to content

Commit

Permalink
feat: Overhaul Hot Reloads (#86)
Browse files Browse the repository at this point in the history
* Added new server packages.

This commit open sources a few modules that were internal to Tidbyt and
refactors existing server code into a loader and watcher module. Some
big changes in the refactor include directory watching inside of watcher
to ensure we get every update and removing sync.Mutex in favor of
channels.

* Refactored server to use new packages.

This commit refactors the server code to use our new packages.

* Update build to go1.16 to match go.mod.

This commit updates the test actions to use go 1.16.12 to match the
go.mod and to be able to support go embpedding.
  • Loading branch information
betterengineering authored Dec 23, 2021
1 parent 50d376d commit ce90198
Show file tree
Hide file tree
Showing 14 changed files with 683 additions and 214 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jobs:
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: '1.16.12'

- name: Checkout code
uses: actions/checkout@v2
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/fsnotify/fsnotify v1.5.1
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.4.2
github.com/harukasan/go-libwebp v0.0.0-20190703060927-68562c9c99af
github.com/lucasb-eyer/go-colorful v1.2.0
Expand All @@ -25,5 +26,6 @@ require (
go.starlark.net v0.0.0-20211203141949-70c0e40ae128
golang.org/x/image v0.0.0-20211028202545-6944b10bf410
golang.org/x/net v0.0.0-20210825183410-e898025ed96a // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/text v0.3.7 // indirect
)
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
Expand Down Expand Up @@ -601,6 +603,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand Down
13 changes: 7 additions & 6 deletions serve.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package main

import (
"log"

"github.com/spf13/cobra"

"tidbyt.dev/pixlet/server"
Expand All @@ -25,10 +23,13 @@ var serveCmd = &cobra.Command{
Use: "serve [script]",
Short: "Serves a starlark render script over HTTP.",
Args: cobra.ExactArgs(1),
Run: serve,
RunE: serve,
}

func serve(cmd *cobra.Command, args []string) {
s := server.NewServer(host, port, watch, args[0])
log.Fatal(s.Run())
func serve(cmd *cobra.Command, args []string) error {
s, err := server.NewServer(host, port, watch, args[0])
if err != nil {
return err
}
return s.Run()
}
151 changes: 151 additions & 0 deletions server/browser/browser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Package browser provides the ability to send WebP images to a browser over
// websockets.
package browser

import (
_ "embed"
"fmt"
"html/template"
"log"
"net/http"

"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"golang.org/x/sync/errgroup"
"tidbyt.dev/pixlet/server/fanout"
"tidbyt.dev/pixlet/server/loader"
)

// Browser provides a structure for serving WebP images over websockets to
// a web browser.
type Browser struct {
addr string // The address to listen on.
title string // The title of the HTML document.
updateChan chan string // A channel of base64 encoded WebP images.
watch bool
fo *fanout.Fanout
r *mux.Router
tmpl *template.Template
loader *loader.Loader
}

//go:embed preview-mask.png
var previewMask []byte

//go:embed favicon.png
var favicon []byte

//go:embed preview.html
var previewHTML string

// previewData is used to populate the HTML template.
type previewData struct {
Title string
WebP string
Watch bool
}

// NewBrowser sets up a browser structure. Call Run() to kick off the main loops.
func NewBrowser(addr string, title string, watch bool, updateChan chan string, l *loader.Loader) (*Browser, error) {
tmpl, err := template.New("preview").Parse(previewHTML)
if err != nil {
return nil, err
}

b := &Browser{
updateChan: updateChan,
addr: addr,
fo: fanout.NewFanout(),
tmpl: tmpl,
title: title,
loader: l,
watch: watch,
}

r := mux.NewRouter()
r.HandleFunc("/", b.rootHandler)
r.HandleFunc("/ws", b.websocketHandler)
r.HandleFunc("/favicon.png", b.faviconHandler)
r.HandleFunc("/preview-mask.png", b.previewMaskHandler)
b.r = r

return b, nil
}

// Run starts the server process and runs forever in a blocking fashion. The
// main routines include an update watcher to process incomming changes to the
// webp and running the http handlers.
func (b *Browser) Run() error {
defer b.fo.Quit()

g := errgroup.Group{}
g.Go(b.updateWatcher)
g.Go(b.serveHTTP)

return g.Wait()
}

func (b *Browser) serveHTTP() error {
log.Printf("listening at http://%s\n", b.addr)
return http.ListenAndServe(b.addr, b.r)
}

func (b *Browser) faviconHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/png")
w.Write(favicon)
}

func (b *Browser) previewMaskHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/png")
w.Write(previewMask)
}

func (b *Browser) websocketHandler(w http.ResponseWriter, r *http.Request) {
if !b.watch {
return
}

var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("error establishing a new connection %v\n", err)
return
}

b.fo.NewClient(conn)
}

func (b *Browser) updateWatcher() error {
for {
select {
case webp := <-b.updateChan:
b.fo.Broadcast(webp)
}
}
}

func (b *Browser) rootHandler(w http.ResponseWriter, r *http.Request) {
config := make(map[string]string)
for k, vals := range r.URL.Query() {
config[k] = vals[0]
}

webp, err := b.loader.LoadApplet(config)
if err != nil {
w.WriteHeader(500)
fmt.Fprintln(w, err)
return
}

data := previewData{
Title: b.title,
Watch: b.watch,
WebP: webp,
}

w.Header().Set("Content-Type", "text/html")
b.tmpl.Execute(w, data)
}
Binary file added server/browser/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added server/browser/preview-mask.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
95 changes: 95 additions & 0 deletions server/browser/preview.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<!DOCTYPE html>
<html>

<head>
<title>{{ .Title }}</title>
<link rel="icon" type="image/png" href="/favicon.png" />
<style type="text/css">
img {
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
width: 100%;
mask-size: contain;
-webkit-mask-size: contain;
mask-image: url("./preview-mask.png");
-webkit-mask-image: url("./preview-mask.png");
}
</style>
</head>

<body bgcolor="black">
<div style="border: solid 1px white">
<img id="render" src="data:image/webp;base64,{{ .WebP }}" />
</div>

{{ if .Watch }}
<script>
class Watcher {
constructor() {
this.connect();
}

connect() {
this.conn = new WebSocket("ws://" + document.location.host + "/ws");
this.conn.open = this.open.bind(this);
this.conn.onmessage = this.process.bind(this);
this.conn.onclose = this.close.bind(this);
setTimeout(this.check.bind(this), 5000)
}

open(e) {
console.log("connection established");
}

process(e) {
console.log("recieved new message");
const data = JSON.parse(e.data);

switch (data.type) {
case "webp":
const img = document.getElementById("render");
img.src = "data:image/webp;base64," + data.message;
}
}

check() {
if (this.conn.readyState === WebSocket.CONNECTING) {
console.log("connection timed out");
this.reconnect();
}
}

close(e) {
// If the underlying TCP connection is having issues,
// refresh the entire page.
console.log("connection closed", e.code);
if (e.code === 1006) {
// If the server is down, the browser has a tough time
// refreshing when it actually comes back up. Keep
// refreshing until this js is no longer loaded.
setInterval(this.refresh.bind(this), 5000)
this.refresh();
return;
}

this.reconnect();
}

refresh() {
console.log("attempting to refresh page");
location.reload(true);
}

reconnect() {
console.log("reestablishing connection");
this.connect();
}
}

let watcher = new Watcher();
</script>
{{ end }}
</body>

</html>
Loading

0 comments on commit ce90198

Please sign in to comment.