Skip to content

Commit 461a072

Browse files
authored
Merge pull request #4 from x1unix/feat/wasm-support
🎉 Add WebAssembly support
2 parents 1f1b172 + 6399404 commit 461a072

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2681
-130
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ run:
1616

1717
.PHONY:ui
1818
ui:
19-
@cd $(UI) && REACT_APP_LANG_SERVER=http://$(LISTEN_ADDR) yarn start
19+
@cd $(UI) && REACT_APP_LANG_SERVER=http://$(LISTEN_ADDR) REACT_APP_VERSION=testing yarn start
2020

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ Improved Go Playground powered by Monaco Editor and React
1111

1212
* 💡 Code autocomplete
1313
* 💾 Load and save files
14+
* 🛠 [WebAssembly](https://github.com/golang/go/wiki/WebAssembly) support
1415
* 🌚 Dark theme
1516

17+
1618
And more
1719

1820
## Demo

build/Dockerfile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
FROM node:13-alpine as ui-build
22
COPY web /tmp/web
33
WORKDIR /tmp/web
4-
RUN yarn install --silent && yarn build
4+
ARG APP_VERSION="1.0.0"
5+
ARG GITHUB_URL="https://github.com/x1unix/go-playground"
6+
RUN yarn install --silent && REACT_APP_VERSION=$APP_VERSION REACT_APP_GITHUB_URL=$GITHUB_URL yarn build
57

68
FROM golang:1.13-alpine as build
79
WORKDIR /tmp/playground
@@ -14,8 +16,10 @@ RUN go build -o server ./cmd/playground
1416
FROM golang:1.13-alpine as production
1517
WORKDIR /opt/playground
1618
ENV GOROOT /usr/local/go
19+
ENV APP_CLEAN_INTERVAL=10m
20+
ENV APP_DEBUG=false
1721
COPY data ./data
1822
COPY --from=ui-build /tmp/web/build ./public
1923
COPY --from=build /tmp/playground/server .
2024
EXPOSE 8000
21-
ENTRYPOINT ["/opt/playground/server", "-f=/opt/playground/data/packages.json", "-addr=:8000"]
25+
ENTRYPOINT /opt/playground/server -f=/opt/playground/data/packages.json -addr=:8000 -clean-interval=${APP_CLEAN_INTERVAL} -debug=${APP_DEBUG}

cmd/playground/main.go

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,42 @@
11
package main
22

33
import (
4+
"context"
45
"flag"
56
"fmt"
7+
"net/http"
8+
"os"
9+
"sync"
10+
"time"
11+
612
"github.com/gorilla/mux"
13+
"github.com/x1unix/foundation/app"
714
"github.com/x1unix/go-playground/pkg/analyzer"
15+
"github.com/x1unix/go-playground/pkg/compiler"
16+
"github.com/x1unix/go-playground/pkg/compiler/storage"
817
"github.com/x1unix/go-playground/pkg/langserver"
918
"go.uber.org/zap"
10-
"log"
11-
"net/http"
12-
"os"
1319
)
1420

21+
type appArgs struct {
22+
packagesFile string
23+
addr string
24+
debug bool
25+
buildDir string
26+
cleanupInterval string
27+
}
28+
29+
func (a appArgs) getCleanDuration() (time.Duration, error) {
30+
return time.ParseDuration(a.cleanupInterval)
31+
}
32+
1533
func main() {
16-
var packagesFile string
17-
var addr string
18-
var debug bool
19-
flag.StringVar(&packagesFile, "f", "packages.json", "Path to packages index JSON file")
20-
flag.StringVar(&addr, "addr", ":8080", "TCP Listen address")
21-
flag.BoolVar(&debug, "debug", false, "Enable debug mode")
34+
args := appArgs{}
35+
flag.StringVar(&args.packagesFile, "f", "packages.json", "Path to packages index JSON file")
36+
flag.StringVar(&args.addr, "addr", ":8080", "TCP Listen address")
37+
flag.StringVar(&args.buildDir, "wasm-build-dir", os.TempDir(), "Directory for WASM builds")
38+
flag.StringVar(&args.cleanupInterval, "clean-interval", "10m", "Build directory cleanup interval")
39+
flag.BoolVar(&args.debug, "debug", false, "Enable debug mode")
2240

2341
goRoot, ok := os.LookupEnv("GOROOT")
2442
if !ok {
@@ -27,9 +45,9 @@ func main() {
2745
}
2846

2947
flag.Parse()
30-
l := getLogger(debug)
48+
l := getLogger(args.debug)
3149
defer l.Sync()
32-
if err := start(packagesFile, addr, goRoot, debug); err != nil {
50+
if err := start(goRoot, args); err != nil {
3351
l.Sugar().Fatal(err)
3452
}
3553
}
@@ -51,31 +69,78 @@ func getLogger(debug bool) (l *zap.Logger) {
5169
return l
5270
}
5371

54-
func start(packagesFile, addr, goRoot string, debug bool) error {
72+
func start(goRoot string, args appArgs) error {
73+
cleanInterval, err := args.getCleanDuration()
74+
if err != nil {
75+
return fmt.Errorf("invalid cleanup interval parameter: %s", err)
76+
}
77+
5578
zap.S().Infof("GOROOT is %q", goRoot)
56-
zap.S().Infof("Packages file is %q", packagesFile)
79+
zap.S().Infof("Packages file is %q", args.packagesFile)
80+
zap.S().Infof("Cleanup interval is %s", cleanInterval.String())
5781
analyzer.SetRoot(goRoot)
58-
packages, err := analyzer.ReadPackagesFile(packagesFile)
82+
packages, err := analyzer.ReadPackagesFile(args.packagesFile)
83+
if err != nil {
84+
return fmt.Errorf("failed to read packages file %q: %s", args.packagesFile, err)
85+
}
86+
87+
store, err := storage.NewLocalStorage(zap.S(), args.buildDir)
5988
if err != nil {
60-
return fmt.Errorf("failed to read packages file %q: %s", packagesFile, err)
89+
return err
6190
}
6291

92+
ctx, _ := app.GetApplicationContext()
93+
wg := &sync.WaitGroup{}
94+
go store.StartCleaner(ctx, cleanInterval, nil)
95+
6396
r := mux.NewRouter()
64-
langserver.New(packages).Mount(r.PathPrefix("/api").Subrouter())
97+
langserver.New(packages, compiler.NewBuildService(zap.S(), store)).
98+
Mount(r.PathPrefix("/api").Subrouter())
6599
r.PathPrefix("/").Handler(langserver.SpaFileServer("./public"))
66100

67-
zap.S().Infof("Listening on %q", addr)
68-
69101
var handler http.Handler
70-
if debug {
102+
if args.debug {
71103
zap.S().Info("Debug mode enabled, CORS disabled")
72104
handler = langserver.NewCORSDisablerWrapper(r)
73105
} else {
74106
handler = r
75107
}
76108

77-
if err := http.ListenAndServe(addr, handler); err != nil {
78-
log.Fatal(err)
109+
server := &http.Server{
110+
Addr: args.addr,
111+
Handler: handler,
112+
ReadTimeout: 5 * time.Second,
113+
WriteTimeout: 10 * time.Second,
114+
IdleTimeout: 15 * time.Second,
115+
}
116+
117+
if err := startHttpServer(ctx, wg, server); err != nil {
118+
return err
119+
}
120+
121+
wg.Wait()
122+
return nil
123+
}
124+
125+
func startHttpServer(ctx context.Context, wg *sync.WaitGroup, server *http.Server) error {
126+
logger := zap.S()
127+
go func() {
128+
<-ctx.Done()
129+
logger.Info("Shutting down server...")
130+
shutdownCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
131+
defer cancel()
132+
defer wg.Done()
133+
server.SetKeepAlivesEnabled(false)
134+
if err := server.Shutdown(shutdownCtx); err != nil {
135+
logger.Errorf("Could not gracefully shutdown the server: %v\n", err)
136+
}
137+
return
138+
}()
139+
140+
wg.Add(1)
141+
logger.Infof("Listening on %q", server.Addr)
142+
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
143+
return fmt.Errorf("cannot start server on %q: %s", server.Addr, err)
79144
}
80145

81146
return nil

docker.mk

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ DOCKERFILE ?= ./build/Dockerfile
22
IMG_NAME ?= x1unix/go-playground
33

44
.PHONY: docker
5-
docker: docker-login docker-make-image
5+
docker: docker-login docker-image
66
@echo "- Pushing $(IMG_NAME):$(TAG) (as latest)..."
77
docker push $(IMG_NAME):$(TAG)
88
docker push $(IMG_NAME):latest
@@ -17,10 +17,10 @@ docker-login:
1717
fi;
1818
@docker login -u $(DOCKER_USER) -p $(DOCKER_PASS) && echo "- Docker login success";
1919

20-
.PHONY: docker-make-image
21-
docker-make-image:
20+
.PHONY: docker-image
21+
docker-image:
2222
@if [ -z "$(TAG)" ]; then\
2323
echo "required parameter TAG is undefined" && exit 1; \
2424
fi;
2525
@echo "- Building '$(IMG_NAME):latest' $(TAG)..."
26-
docker image build -t $(IMG_NAME):latest -t $(IMG_NAME):$(TAG) -f $(DOCKERFILE) .
26+
docker image build -t $(IMG_NAME):latest -t $(IMG_NAME):$(TAG) -f $(DOCKERFILE) --build-arg APP_VERSION=$(TAG) .

go.mod

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ go 1.13
44

55
require (
66
github.com/gorilla/mux v1.7.3
7-
github.com/gorilla/websocket v1.4.1
7+
github.com/pkg/errors v0.8.1
8+
github.com/stretchr/testify v1.4.0
9+
github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5
10+
github.com/x1unix/foundation v1.0.0
811
go.uber.org/atomic v1.5.1 // indirect
912
go.uber.org/multierr v1.4.0 // indirect
1013
go.uber.org/zap v1.13.0
1114
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect
15+
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
1216
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69 // indirect
1317
)

go.sum

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
11
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
22
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
33
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
45
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
56
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
67
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
78
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
8-
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
9-
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
109
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
10+
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
1111
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
1212
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
13+
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
1314
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
15+
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
1416
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
17+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1518
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
1619
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
1720
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
1821
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
22+
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
1923
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
24+
github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5 h1:hNna6Fi0eP1f2sMBe/rJicDmaHmoXGe1Ta84FPYHLuE=
25+
github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5/go.mod h1:f1SCnEOt6sc3fOJfPQDRDzHOtSXuTtnz0ImG9kPRDV0=
26+
github.com/x1unix/foundation v1.0.0 h1:tG0dG1sbiF9TGrjwns+wtX5feBprRD5iTvpmgQDnacA=
27+
github.com/x1unix/foundation v1.0.0/go.mod h1:y9E4igeUWi+njm4xCM48NItLhVH/Jj1KGE069I3J5Hc=
2028
go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=
2129
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
2230
go.uber.org/atomic v1.5.1 h1:rsqfU5vBkVknbhUGbAUwQKR2H4ItV8tjJ+6kJX4cxHM=
@@ -45,6 +53,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
4553
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
4654
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
4755
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
56+
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
57+
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
4858
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
4959
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
5060
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -56,8 +66,10 @@ golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapK
5666
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
5767
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
5868
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
69+
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
5970
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
6071
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
72+
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
6173
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
6274
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
6375
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=

pkg/compiler/compiler.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package compiler
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"github.com/x1unix/go-playground/pkg/compiler/storage"
7+
"go.uber.org/zap"
8+
"io"
9+
"os"
10+
"os/exec"
11+
)
12+
13+
var buildArgs = []string{
14+
"CGO_ENABLED=0",
15+
"GOOS=js",
16+
"GOARCH=wasm",
17+
"HOME=" + os.Getenv("HOME"),
18+
}
19+
20+
type Result struct {
21+
FileName string
22+
}
23+
24+
type BuildService struct {
25+
log *zap.SugaredLogger
26+
storage storage.StoreProvider
27+
}
28+
29+
func NewBuildService(log *zap.SugaredLogger, store storage.StoreProvider) BuildService {
30+
return BuildService{
31+
log: log.Named("builder"),
32+
storage: store,
33+
}
34+
}
35+
36+
func (s BuildService) buildSource(ctx context.Context, outputLocation, sourceLocation string) error {
37+
cmd := exec.CommandContext(ctx, "go",
38+
"build",
39+
"-o",
40+
outputLocation,
41+
sourceLocation,
42+
)
43+
44+
cmd.Env = buildArgs
45+
buff := &bytes.Buffer{}
46+
cmd.Stderr = buff
47+
48+
s.log.Debugw("starting go build", "command", cmd.Args, "env", cmd.Env)
49+
if err := cmd.Start(); err != nil {
50+
return err
51+
}
52+
53+
if err := cmd.Wait(); err != nil {
54+
errMsg := buff.String()
55+
s.log.Debugw("build failed", "err", err, "stderr", errMsg)
56+
return newBuildError(errMsg)
57+
}
58+
59+
return nil
60+
}
61+
62+
func (s BuildService) GetArtifact(id storage.ArtifactID) (io.ReadCloser, error) {
63+
return s.storage.GetItem(id)
64+
}
65+
66+
func (s BuildService) Build(ctx context.Context, data []byte) (*Result, error) {
67+
aid, err := storage.GetArtifactID(data)
68+
if err != nil {
69+
return nil, err
70+
}
71+
72+
result := &Result{FileName: aid.Ext(storage.ExtWasm)}
73+
isCached, err := s.storage.HasItem(aid)
74+
if err != nil {
75+
s.log.Errorw("failed to check cache", "artifact", aid.String(), "err", err)
76+
return nil, err
77+
}
78+
79+
if isCached {
80+
// Just return precompiled result if data is cached already
81+
s.log.Debugw("build cached, returning cached file", "artifact", aid.String())
82+
return result, nil
83+
}
84+
85+
err = s.storage.CreateLocationAndDo(aid, data, func(wasmLocation, sourceLocation string) error {
86+
return s.buildSource(ctx, wasmLocation, sourceLocation)
87+
})
88+
89+
return result, err
90+
}

0 commit comments

Comments
 (0)