Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b1f1cba
ui: add settings and about modals
x1unix Jan 25, 2020
8556fc6
ui: settings change detection
x1unix Jan 25, 2020
4f35f5f
ui: settings change detection
x1unix Jan 25, 2020
3c35471
ui: show WASM disclaimer
x1unix Jan 25, 2020
827f069
working on embedded Go runner
x1unix Jan 25, 2020
f4cbbc1
go-wasm: add Go wasm to Storage adapter
x1unix Jan 25, 2020
13f11a3
go-wasm: fix warnings
x1unix Jan 25, 2020
140738c
server: add build service
x1unix Jan 26, 2020
d5ce6bd
server: fix some storage errors and add tests
x1unix Jan 26, 2020
9961cef
storage: change default permissions
x1unix Jan 26, 2020
5b99b9c
compiler: add to handler
x1unix Jan 27, 2020
8d8bbe4
server: fix response status for wasm
x1unix Jan 27, 2020
45c8ca8
ui: run wasm go
x1unix Jan 27, 2020
76713e9
go-wasm: use patched "wasm_exec.js" from Go SDK
x1unix Jan 27, 2020
ee34747
ui: fix preview overflow
x1unix Jan 27, 2020
f0c273c
ui: preview - show timers and exit status only for Go playground build
x1unix Jan 27, 2020
7c28dac
ui: reducers - reset build output if build runtime changed
x1unix Jan 27, 2020
5177814
ui: sync monaco and runtime with local storage
x1unix Jan 27, 2020
0c2a39a
ui: set format param on build
x1unix Jan 27, 2020
3d78fc6
server: respect format param
x1unix Jan 27, 2020
fe3e58f
return reformatted code for wasm
x1unix Jan 27, 2020
dc7f456
server: add garbage collector and graceful shutdown
x1unix Jan 27, 2020
5bfbc52
server: don't wait for GC to finish
x1unix Jan 27, 2020
c977d4a
server: clean storage at init if dir is empty
x1unix Jan 27, 2020
c2f3158
docker: pass version and clean interval
x1unix Jan 27, 2020
86c7586
make: rename recipe make-docker-image
x1unix Jan 27, 2020
3b4269b
fix local_test.go
x1unix Jan 27, 2020
cf4fd49
ui: set dev version
x1unix Jan 27, 2020
9e7b8cb
docker: pass version and debug params
x1unix Jan 27, 2020
6399404
update README
x1unix Jan 27, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ run:

.PHONY:ui
ui:
@cd $(UI) && REACT_APP_LANG_SERVER=http://$(LISTEN_ADDR) yarn start
@cd $(UI) && REACT_APP_LANG_SERVER=http://$(LISTEN_ADDR) REACT_APP_VERSION=testing yarn start

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ Improved Go Playground powered by Monaco Editor and React

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


And more

## Demo
Expand Down
8 changes: 6 additions & 2 deletions build/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
FROM node:13-alpine as ui-build
COPY web /tmp/web
WORKDIR /tmp/web
RUN yarn install --silent && yarn build
ARG APP_VERSION="1.0.0"
ARG GITHUB_URL="https://github.com/x1unix/go-playground"
RUN yarn install --silent && REACT_APP_VERSION=$APP_VERSION REACT_APP_GITHUB_URL=$GITHUB_URL yarn build

FROM golang:1.13-alpine as build
WORKDIR /tmp/playground
Expand All @@ -14,8 +16,10 @@ RUN go build -o server ./cmd/playground
FROM golang:1.13-alpine as production
WORKDIR /opt/playground
ENV GOROOT /usr/local/go
ENV APP_CLEAN_INTERVAL=10m
ENV APP_DEBUG=false
COPY data ./data
COPY --from=ui-build /tmp/web/build ./public
COPY --from=build /tmp/playground/server .
EXPOSE 8000
ENTRYPOINT ["/opt/playground/server", "-f=/opt/playground/data/packages.json", "-addr=:8000"]
ENTRYPOINT /opt/playground/server -f=/opt/playground/data/packages.json -addr=:8000 -clean-interval=${APP_CLEAN_INTERVAL} -debug=${APP_DEBUG}
107 changes: 86 additions & 21 deletions cmd/playground/main.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,42 @@
package main

import (
"context"
"flag"
"fmt"
"net/http"
"os"
"sync"
"time"

"github.com/gorilla/mux"
"github.com/x1unix/foundation/app"
"github.com/x1unix/go-playground/pkg/analyzer"
"github.com/x1unix/go-playground/pkg/compiler"
"github.com/x1unix/go-playground/pkg/compiler/storage"
"github.com/x1unix/go-playground/pkg/langserver"
"go.uber.org/zap"
"log"
"net/http"
"os"
)

type appArgs struct {
packagesFile string
addr string
debug bool
buildDir string
cleanupInterval string
}

func (a appArgs) getCleanDuration() (time.Duration, error) {
return time.ParseDuration(a.cleanupInterval)
}

func main() {
var packagesFile string
var addr string
var debug bool
flag.StringVar(&packagesFile, "f", "packages.json", "Path to packages index JSON file")
flag.StringVar(&addr, "addr", ":8080", "TCP Listen address")
flag.BoolVar(&debug, "debug", false, "Enable debug mode")
args := appArgs{}
flag.StringVar(&args.packagesFile, "f", "packages.json", "Path to packages index JSON file")
flag.StringVar(&args.addr, "addr", ":8080", "TCP Listen address")
flag.StringVar(&args.buildDir, "wasm-build-dir", os.TempDir(), "Directory for WASM builds")
flag.StringVar(&args.cleanupInterval, "clean-interval", "10m", "Build directory cleanup interval")
flag.BoolVar(&args.debug, "debug", false, "Enable debug mode")

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

flag.Parse()
l := getLogger(debug)
l := getLogger(args.debug)
defer l.Sync()
if err := start(packagesFile, addr, goRoot, debug); err != nil {
if err := start(goRoot, args); err != nil {
l.Sugar().Fatal(err)
}
}
Expand All @@ -51,31 +69,78 @@ func getLogger(debug bool) (l *zap.Logger) {
return l
}

func start(packagesFile, addr, goRoot string, debug bool) error {
func start(goRoot string, args appArgs) error {
cleanInterval, err := args.getCleanDuration()
if err != nil {
return fmt.Errorf("invalid cleanup interval parameter: %s", err)
}

zap.S().Infof("GOROOT is %q", goRoot)
zap.S().Infof("Packages file is %q", packagesFile)
zap.S().Infof("Packages file is %q", args.packagesFile)
zap.S().Infof("Cleanup interval is %s", cleanInterval.String())
analyzer.SetRoot(goRoot)
packages, err := analyzer.ReadPackagesFile(packagesFile)
packages, err := analyzer.ReadPackagesFile(args.packagesFile)
if err != nil {
return fmt.Errorf("failed to read packages file %q: %s", args.packagesFile, err)
}

store, err := storage.NewLocalStorage(zap.S(), args.buildDir)
if err != nil {
return fmt.Errorf("failed to read packages file %q: %s", packagesFile, err)
return err
}

ctx, _ := app.GetApplicationContext()
wg := &sync.WaitGroup{}
go store.StartCleaner(ctx, cleanInterval, nil)

r := mux.NewRouter()
langserver.New(packages).Mount(r.PathPrefix("/api").Subrouter())
langserver.New(packages, compiler.NewBuildService(zap.S(), store)).
Mount(r.PathPrefix("/api").Subrouter())
r.PathPrefix("/").Handler(langserver.SpaFileServer("./public"))

zap.S().Infof("Listening on %q", addr)

var handler http.Handler
if debug {
if args.debug {
zap.S().Info("Debug mode enabled, CORS disabled")
handler = langserver.NewCORSDisablerWrapper(r)
} else {
handler = r
}

if err := http.ListenAndServe(addr, handler); err != nil {
log.Fatal(err)
server := &http.Server{
Addr: args.addr,
Handler: handler,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 15 * time.Second,
}

if err := startHttpServer(ctx, wg, server); err != nil {
return err
}

wg.Wait()
return nil
}

func startHttpServer(ctx context.Context, wg *sync.WaitGroup, server *http.Server) error {
logger := zap.S()
go func() {
<-ctx.Done()
logger.Info("Shutting down server...")
shutdownCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
defer wg.Done()
server.SetKeepAlivesEnabled(false)
if err := server.Shutdown(shutdownCtx); err != nil {
logger.Errorf("Could not gracefully shutdown the server: %v\n", err)
}
return
}()

wg.Add(1)
logger.Infof("Listening on %q", server.Addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return fmt.Errorf("cannot start server on %q: %s", server.Addr, err)
}

return nil
Expand Down
8 changes: 4 additions & 4 deletions docker.mk
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ DOCKERFILE ?= ./build/Dockerfile
IMG_NAME ?= x1unix/go-playground

.PHONY: docker
docker: docker-login docker-make-image
docker: docker-login docker-image
@echo "- Pushing $(IMG_NAME):$(TAG) (as latest)..."
docker push $(IMG_NAME):$(TAG)
docker push $(IMG_NAME):latest
Expand All @@ -17,10 +17,10 @@ docker-login:
fi;
@docker login -u $(DOCKER_USER) -p $(DOCKER_PASS) && echo "- Docker login success";

.PHONY: docker-make-image
docker-make-image:
.PHONY: docker-image
docker-image:
@if [ -z "$(TAG)" ]; then\
echo "required parameter TAG is undefined" && exit 1; \
fi;
@echo "- Building '$(IMG_NAME):latest' $(TAG)..."
docker image build -t $(IMG_NAME):latest -t $(IMG_NAME):$(TAG) -f $(DOCKERFILE) .
docker image build -t $(IMG_NAME):latest -t $(IMG_NAME):$(TAG) -f $(DOCKERFILE) --build-arg APP_VERSION=$(TAG) .
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ go 1.13

require (
github.com/gorilla/mux v1.7.3
github.com/gorilla/websocket v1.4.1
github.com/pkg/errors v0.8.1
github.com/stretchr/testify v1.4.0
github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5
github.com/x1unix/foundation v1.0.0
go.uber.org/atomic v1.5.1 // indirect
go.uber.org/multierr v1.4.0 // indirect
go.uber.org/zap v1.13.0
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69 // indirect
)
16 changes: 14 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5 h1:hNna6Fi0eP1f2sMBe/rJicDmaHmoXGe1Ta84FPYHLuE=
github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5/go.mod h1:f1SCnEOt6sc3fOJfPQDRDzHOtSXuTtnz0ImG9kPRDV0=
github.com/x1unix/foundation v1.0.0 h1:tG0dG1sbiF9TGrjwns+wtX5feBprRD5iTvpmgQDnacA=
github.com/x1unix/foundation v1.0.0/go.mod h1:y9E4igeUWi+njm4xCM48NItLhVH/Jj1KGE069I3J5Hc=
go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.5.1 h1:rsqfU5vBkVknbhUGbAUwQKR2H4ItV8tjJ+6kJX4cxHM=
Expand Down Expand Up @@ -45,6 +53,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
Expand All @@ -56,8 +66,10 @@ golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapK
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
90 changes: 90 additions & 0 deletions pkg/compiler/compiler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package compiler

import (
"bytes"
"context"
"github.com/x1unix/go-playground/pkg/compiler/storage"
"go.uber.org/zap"
"io"
"os"
"os/exec"
)

var buildArgs = []string{
"CGO_ENABLED=0",
"GOOS=js",
"GOARCH=wasm",
"HOME=" + os.Getenv("HOME"),
}

type Result struct {
FileName string
}

type BuildService struct {
log *zap.SugaredLogger
storage storage.StoreProvider
}

func NewBuildService(log *zap.SugaredLogger, store storage.StoreProvider) BuildService {
return BuildService{
log: log.Named("builder"),
storage: store,
}
}

func (s BuildService) buildSource(ctx context.Context, outputLocation, sourceLocation string) error {
cmd := exec.CommandContext(ctx, "go",
"build",
"-o",
outputLocation,
sourceLocation,
)

cmd.Env = buildArgs
buff := &bytes.Buffer{}
cmd.Stderr = buff

s.log.Debugw("starting go build", "command", cmd.Args, "env", cmd.Env)
if err := cmd.Start(); err != nil {
return err
}

if err := cmd.Wait(); err != nil {
errMsg := buff.String()
s.log.Debugw("build failed", "err", err, "stderr", errMsg)
return newBuildError(errMsg)
}

return nil
}

func (s BuildService) GetArtifact(id storage.ArtifactID) (io.ReadCloser, error) {
return s.storage.GetItem(id)
}

func (s BuildService) Build(ctx context.Context, data []byte) (*Result, error) {
aid, err := storage.GetArtifactID(data)
if err != nil {
return nil, err
}

result := &Result{FileName: aid.Ext(storage.ExtWasm)}
isCached, err := s.storage.HasItem(aid)
if err != nil {
s.log.Errorw("failed to check cache", "artifact", aid.String(), "err", err)
return nil, err
}

if isCached {
// Just return precompiled result if data is cached already
s.log.Debugw("build cached, returning cached file", "artifact", aid.String())
return result, nil
}

err = s.storage.CreateLocationAndDo(aid, data, func(wasmLocation, sourceLocation string) error {
return s.buildSource(ctx, wasmLocation, sourceLocation)
})

return result, err
}
Loading