Embed version number, force client refresh on mismatch
This commit is contained in:
parent
987b6a9dfc
commit
02766918f9
13
Dockerfile
13
Dockerfile
|
@ -1,8 +1,19 @@
|
||||||
FROM node:14 AS JS_BUILD
|
FROM node:14 AS JS_BUILD
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends jq moreutils && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /frontend
|
WORKDIR /frontend
|
||||||
COPY ./frontend/package.json ./frontend/yarn.lock ./
|
COPY ./frontend/package.json ./frontend/yarn.lock ./
|
||||||
RUN yarn install --frozen-lockfile
|
RUN yarn install --frozen-lockfile
|
||||||
COPY ./frontend ./
|
COPY ./frontend ./
|
||||||
|
|
||||||
|
# If using environment variables (REACT_APP_*) to pass data in at build-time,
|
||||||
|
# sometimes the values end up in the library code bundles. This means that changing
|
||||||
|
# the verison (as below) would invalidate everyone's caches even if the actual code
|
||||||
|
# didn't change. To avoid this, resort to using a JSON file that is edited before build.
|
||||||
|
ARG version
|
||||||
|
RUN jq ".version = \"${version}\"" ./src/metadata.json | sponge ./src/metadata.json
|
||||||
RUN yarn build
|
RUN yarn build
|
||||||
|
|
||||||
FROM golang:1.14 as GO_BUILD
|
FROM golang:1.14 as GO_BUILD
|
||||||
|
@ -21,5 +32,5 @@ FROM gcr.io/distroless/base:nonroot
|
||||||
WORKDIR /codies
|
WORKDIR /codies
|
||||||
COPY --from=GO_BUILD /codies/codies ./codies
|
COPY --from=GO_BUILD /codies/codies ./codies
|
||||||
COPY --from=JS_BUILD /frontend/build ./frontend/build
|
COPY --from=JS_BUILD /frontend/build ./frontend/build
|
||||||
ENTRYPOINT [ "/codies/codies" ]
|
ENTRYPOINT [ "/codies/codies", "--prod" ]
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
|
@ -30,3 +30,8 @@ export function assertIsDefined<T>(val: T): asserts val is NonNullable<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const nameofFactory = <T>() => (name: keyof T) => name;
|
export const nameofFactory = <T>() => (name: keyof T) => name;
|
||||||
|
|
||||||
|
export function reloadOutdatedPage() {
|
||||||
|
console.log('Frontend version appears to be outdated; reloading to allow the browser to update.');
|
||||||
|
window.location.reload(true);
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {
|
||||||
import { Help } from '@material-ui/icons';
|
import { Help } from '@material-ui/icons';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { version } from '../metadata.json';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
modal: {
|
modal: {
|
||||||
|
@ -80,7 +82,8 @@ export const AboutButton = (props: { style?: React.CSSProperties }) => {
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
You can find this site's code on{' '}
|
You can find this site's code on{' '}
|
||||||
<NewPageLink href="https://github.com/zikaeroh/codies">GitHub</NewPageLink>.
|
<NewPageLink href="https://github.com/zikaeroh/codies">GitHub</NewPageLink>. This site is
|
||||||
|
currently running version {version}.
|
||||||
</p>
|
</p>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Fade>
|
</Fade>
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"version": "(devel)"
|
||||||
|
}
|
|
@ -3,8 +3,9 @@ import * as React from 'react';
|
||||||
import useWebSocket from 'react-use-websocket';
|
import useWebSocket from 'react-use-websocket';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { assertIsDefined, assertNever, noop, websocketUrl } from '../common';
|
import { assertIsDefined, assertNever, noop, reloadOutdatedPage, websocketUrl } from '../common';
|
||||||
import { useServerTime } from '../hooks/useServerTime';
|
import { useServerTime } from '../hooks/useServerTime';
|
||||||
|
import { version } from '../metadata.json';
|
||||||
import { ClientNote, ServerNote, State, StatePlayer, TimeResponse, WordPack } from '../protocol';
|
import { ClientNote, ServerNote, State, StatePlayer, TimeResponse, WordPack } from '../protocol';
|
||||||
import { GameView, Sender } from './gameView';
|
import { GameView, Sender } from './gameView';
|
||||||
import { Loading } from './loading';
|
import { Loading } from './loading';
|
||||||
|
@ -65,12 +66,22 @@ function useWS(roomID: string, playerID: string, nickname: string, dead: () => v
|
||||||
const retry = React.useRef(0);
|
const retry = React.useRef(0);
|
||||||
|
|
||||||
return useWebSocket(socketUrl, {
|
return useWebSocket(socketUrl, {
|
||||||
queryParams: { roomID, playerID, nickname },
|
// The names here matter; explicitly naming them so that renaming
|
||||||
|
// these variables doesn't change the actual wire names.
|
||||||
|
//
|
||||||
|
// X-CODIES-VERSION would be cleaner, but the WS hook doesn't
|
||||||
|
// support anything but query params.
|
||||||
|
queryParams: { roomID: roomID, playerID: playerID, nickname: nickname, codiesVersion: version },
|
||||||
reconnectAttempts,
|
reconnectAttempts,
|
||||||
onMessage: () => {
|
onMessage: () => {
|
||||||
retry.current = 0;
|
retry.current = 0;
|
||||||
},
|
},
|
||||||
onOpen,
|
onOpen,
|
||||||
|
onClose: (e: CloseEvent) => {
|
||||||
|
if (e.code === 4418) {
|
||||||
|
reloadOutdatedPage();
|
||||||
|
}
|
||||||
|
},
|
||||||
shouldReconnect: () => {
|
shouldReconnect: () => {
|
||||||
if (didUnmount.current) {
|
if (didUnmount.current) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -3,10 +3,19 @@ import isArray from 'lodash/isArray';
|
||||||
import querystring from 'querystring';
|
import querystring from 'querystring';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { assertIsDefined, isDefined } from '../common';
|
import { assertIsDefined, isDefined, reloadOutdatedPage } from '../common';
|
||||||
import { LoginForm, LoginFormData } from '../components/loginForm';
|
import { LoginForm, LoginFormData } from '../components/loginForm';
|
||||||
|
import { version } from '../metadata.json';
|
||||||
import { RoomResponse } from '../protocol';
|
import { RoomResponse } from '../protocol';
|
||||||
|
|
||||||
|
function checkOutdated(response: Response) {
|
||||||
|
if (response.status === 418) {
|
||||||
|
reloadOutdatedPage();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LoginProps {
|
export interface LoginProps {
|
||||||
onLogin: (roomID: string, nickname: string) => void;
|
onLogin: (roomID: string, nickname: string) => void;
|
||||||
}
|
}
|
||||||
|
@ -69,12 +78,22 @@ export const Login = (props: LoginProps) => {
|
||||||
onSubmit={async (d: LoginFormData) => {
|
onSubmit={async (d: LoginFormData) => {
|
||||||
let id = roomID;
|
let id = roomID;
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'X-CODIES-VERSION': version,
|
||||||
|
};
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
const query = querystring.stringify({
|
const query = querystring.stringify({
|
||||||
roomID: id,
|
roomID: id,
|
||||||
});
|
});
|
||||||
const response = await fetch('/api/exists?' + query);
|
const response = await fetch('/api/exists?' + query, { headers });
|
||||||
|
|
||||||
|
if (checkOutdated(response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await response.text();
|
await response.text();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
setErrorMessage('Room does not exist.');
|
setErrorMessage('Room does not exist.');
|
||||||
setRoomID(undefined);
|
setRoomID(undefined);
|
||||||
|
@ -90,7 +109,12 @@ export const Login = (props: LoginProps) => {
|
||||||
roomPass: d.roomPass,
|
roomPass: d.roomPass,
|
||||||
create: d.create,
|
create: d.create,
|
||||||
});
|
});
|
||||||
response = await fetch('/api/room', { method: 'POST', body: reqBody });
|
response = await fetch('/api/room', { method: 'POST', body: reqBody, headers });
|
||||||
|
|
||||||
|
if (checkOutdated(response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
resp = RoomResponse.parse(body);
|
resp = RoomResponse.parse(body);
|
||||||
// eslint-disable-next-line no-empty
|
// eslint-disable-next-line no-empty
|
||||||
|
|
|
@ -10,3 +10,8 @@ func Version() string {
|
||||||
}
|
}
|
||||||
return version
|
return version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VersionSet returns true if the verison has been set.
|
||||||
|
func VersionSet() bool {
|
||||||
|
return version != ""
|
||||||
|
}
|
||||||
|
|
75
main.go
75
main.go
|
@ -3,6 +3,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -26,11 +27,14 @@ import (
|
||||||
var args = struct {
|
var args = struct {
|
||||||
Addr string `long:"addr" env:"CODIES_ADDR" description:"Address to listen at"`
|
Addr string `long:"addr" env:"CODIES_ADDR" description:"Address to listen at"`
|
||||||
Origins []string `long:"origins" env:"CODIES_ORIGINS" env-delim:"," description:"Additional valid origins for WebSocket connections"`
|
Origins []string `long:"origins" env:"CODIES_ORIGINS" env-delim:"," description:"Additional valid origins for WebSocket connections"`
|
||||||
|
Prod bool `long:"prod" env:"CODIES_PROD" description:"Enables production mode"`
|
||||||
Debug bool `long:"debug" env:"CODIES_DEBUG" description:"Enables debug mode"`
|
Debug bool `long:"debug" env:"CODIES_DEBUG" description:"Enables debug mode"`
|
||||||
}{
|
}{
|
||||||
Addr: ":5000",
|
Addr: ":5000",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var wsOpts *websocket.AcceptOptions
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
rand.Seed(time.Now().Unix())
|
rand.Seed(time.Now().Unix())
|
||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
|
@ -40,15 +44,25 @@ func main() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !args.Prod && !args.Debug {
|
||||||
|
log.Fatal("missing required option --prod or --debug")
|
||||||
|
} else if args.Prod && args.Debug {
|
||||||
|
log.Fatal("must specify either --prod or --debug")
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf("starting codies server, version %s", version.Version())
|
log.Printf("starting codies server, version %s", version.Version())
|
||||||
|
|
||||||
wsOpts := &websocket.AcceptOptions{
|
wsOpts = &websocket.AcceptOptions{
|
||||||
OriginPatterns: args.Origins,
|
OriginPatterns: args.Origins,
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.Debug {
|
if args.Debug {
|
||||||
log.Println("starting in debug mode, allowing any WebSocket origin host")
|
log.Println("starting in debug mode, allowing any WebSocket origin host")
|
||||||
wsOpts.OriginPatterns = []string{"*"}
|
wsOpts.OriginPatterns = []string{"*"}
|
||||||
|
} else {
|
||||||
|
if !version.VersionSet() {
|
||||||
|
log.Fatal("running production build without version set")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
g, ctx := errgroup.WithContext(ctxutil.Interrupt())
|
g, ctx := errgroup.WithContext(ctxutil.Interrupt())
|
||||||
|
@ -68,6 +82,22 @@ func main() {
|
||||||
_ = json.NewEncoder(w).Encode(&protocol.TimeResponse{Time: time.Now()})
|
_ = json.NewEncoder(w).Encode(&protocol.TimeResponse{Time: time.Now()})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
r.Get("/api/stats", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
rooms, clients := srv.Stats()
|
||||||
|
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
_ = enc.Encode(&protocol.StatsResponse{
|
||||||
|
Rooms: rooms,
|
||||||
|
Clients: clients,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
if !args.Debug {
|
||||||
|
r.Use(checkVersion)
|
||||||
|
}
|
||||||
|
|
||||||
r.Get("/api/exists", func(w http.ResponseWriter, r *http.Request) {
|
r.Get("/api/exists", func(w http.ResponseWriter, r *http.Request) {
|
||||||
query := &protocol.ExistsQuery{}
|
query := &protocol.ExistsQuery{}
|
||||||
if err := queryparam.Parse(r.URL.Query(), query); err != nil {
|
if err := queryparam.Parse(r.URL.Query(), query); err != nil {
|
||||||
|
@ -164,16 +194,6 @@ func main() {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Get("/api/stats", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
rooms, clients := srv.Stats()
|
|
||||||
|
|
||||||
enc := json.NewEncoder(w)
|
|
||||||
enc.SetIndent("", " ")
|
|
||||||
_ = enc.Encode(&protocol.StatsResponse{
|
|
||||||
Rooms: rooms,
|
|
||||||
Clients: clients,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -217,6 +237,39 @@ func staticRouter() http.Handler {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkVersion(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
want := version.Version()
|
||||||
|
|
||||||
|
toCheck := []string{
|
||||||
|
r.Header.Get("X-CODIES-VERSION"),
|
||||||
|
r.URL.Query().Get("codiesVersion"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, got := range toCheck {
|
||||||
|
if got == want {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reason := fmt.Sprintf("client version too old, please reload to get %s", want)
|
||||||
|
|
||||||
|
if r.Header.Get("Upgrade") == "websocket" {
|
||||||
|
c, err := websocket.Accept(w, r, wsOpts)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Close(4418, reason)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusTeapot)
|
||||||
|
fmt.Fprint(w, reason)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func httpErr(w http.ResponseWriter, code int) {
|
func httpErr(w http.ResponseWriter, code int) {
|
||||||
http.Error(w, http.StatusText(code), code)
|
http.Error(w, http.StatusText(code), code)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue