Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions build/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ WORKDIR /opt/playground
ENV GOROOT /usr/local/go
ENV APP_CLEAN_INTERVAL=10m
ENV APP_DEBUG=false
ENV APP_PLAYGROUND_URL=https://play.golang.org
ENV APP_PLAYGROUND_URL='https://play.golang.org'
ENV APP_GOTIP_URL='https://gotipplay.golang.org'
COPY data ./data
COPY --from=ui-build /tmp/web/build ./public
COPY --from=build /tmp/playground/server .
COPY --from=build /tmp/playground/worker.wasm ./public
COPY --from=build /tmp/playground/wasm_exec.js ./public
EXPOSE 8000
ENTRYPOINT /opt/playground/server -f=/opt/playground/data/packages.json -addr=:8000 \
-clean-interval=${APP_CLEAN_INTERVAL} -debug=${APP_DEBUG} -playground-url=${APP_PLAYGROUND_URL}
-clean-interval=${APP_CLEAN_INTERVAL} -debug=${APP_DEBUG} -playground-url=${APP_PLAYGROUND_URL} -gotip-url=${APP_GOTIP_URL}
32 changes: 20 additions & 12 deletions cmd/playground/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ import (
var Version = "testing"

type appArgs struct {
packagesFile string
playgroundUrl string
addr string
debug bool
buildDir string
cleanupInterval string
assetsDirectory string
packagesFile string
playgroundURL string
goTipPlaygroundURL string
addr string
debug bool
buildDir string
cleanupInterval string
assetsDirectory string
connectTimeout time.Duration
}

func (a appArgs) getCleanDuration() (time.Duration, error) {
Expand All @@ -49,9 +51,11 @@ func main() {
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.StringVar(&args.playgroundUrl, "playground-url", goplay.DefaultPlaygroundURL, "Go Playground URL")
flag.StringVar(&args.playgroundURL, "playground-url", goplay.DefaultPlaygroundURL, "Go Playground URL")
flag.StringVar(&args.goTipPlaygroundURL, "gotip-url", goplay.DefaultGoTipPlaygroundURL, "GoTip Playground URL")
flag.BoolVar(&args.debug, "debug", false, "Enable debug mode")
flag.StringVar(&args.assetsDirectory, "static-dir", filepath.Join(wd, "public"), "Path to web page assets (HTML, JS, etc)")
flag.DurationVar(&args.connectTimeout, "timeout", 15*time.Second, "Go Playground server connect timeout")

l := getLogger(args.debug)
defer l.Sync() //nolint:errcheck
Expand Down Expand Up @@ -92,7 +96,7 @@ func start(goRoot string, args appArgs) error {

zap.S().Info("Server version: ", Version)
zap.S().Infof("GOROOT is %q", goRoot)
zap.S().Infof("Playground url: %q", args.playgroundUrl)
zap.S().Infof("Playground url: %q", args.playgroundURL)
zap.S().Infof("Packages file is %q", args.packagesFile)
zap.S().Infof("Cleanup interval is %s", cleanInterval.String())
zap.S().Infof("Serving web page from %q", args.assetsDirectory)
Expand All @@ -112,10 +116,14 @@ func start(goRoot string, args appArgs) error {
go store.StartCleaner(ctx, cleanInterval, nil)

r := mux.NewRouter()
pg := goplay.NewClient(args.playgroundUrl, goplay.DefaultUserAgent, 15*time.Second)

pgClient := goplay.NewClient(args.playgroundURL, goplay.DefaultUserAgent, args.connectTimeout)
goTipClient := goplay.NewClient(args.goTipPlaygroundURL, goplay.DefaultUserAgent, args.connectTimeout)
clients := &langserver.PlaygroundServices{
Default: pgClient,
GoTip: goTipClient,
}
// API routes
langserver.New(Version, pg, packages, compiler.NewBuildService(zap.S(), store)).
langserver.New(Version, clients, packages, compiler.NewBuildService(zap.S(), store)).
Mount(r.PathPrefix("/api").Subrouter())

// Web UI routes
Expand Down
5 changes: 3 additions & 2 deletions pkg/goplay/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import (
)

const (
DefaultUserAgent = "goplay.tools/1.0 (http://goplay.tools/)"
DefaultPlaygroundURL = "https://play.golang.org"
DefaultUserAgent = "goplay.tools/1.0 (http://goplay.tools/)"
DefaultPlaygroundURL = "https://play.golang.org"
DefaultGoTipPlaygroundURL = "https://gotipplay.golang.org"

// maxSnippetSize value taken from
// https://github.com/golang/playground/blob/master/app/goplay/share.go
Expand Down
72 changes: 47 additions & 25 deletions pkg/langserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"net/http"
"strconv"
"strings"
"time"

"github.com/gorilla/mux"
Expand All @@ -23,30 +24,37 @@ const (
frameTime = time.Second
maxBuildTimeDuration = time.Second * 30

wasmMimeType = "application/wasm"
formatQueryParam = "format"
artifactParamVal = "artifactId"
wasmMimeType = "application/wasm"
formatQueryParam = "format"
artifactParamVal = "artifactId"
playgroundBackendParam = "backend"
playgroundGoTip = "gotip"
)

type PlaygroundServices struct {
Default *goplay.Client
GoTip *goplay.Client
}

// Service is language server service
type Service struct {
version string
log *zap.SugaredLogger
index analyzer.PackageIndex
compiler compiler.BuildService
playground *goplay.Client
limiter *rate.Limiter
version string
log *zap.SugaredLogger
index analyzer.PackageIndex
compiler compiler.BuildService
playgrounds *PlaygroundServices
limiter *rate.Limiter
}

// New is Service constructor
func New(version string, playground *goplay.Client, packages []*analyzer.Package, builder compiler.BuildService) *Service {
func New(version string, playgrounds *PlaygroundServices, packages []*analyzer.Package, builder compiler.BuildService) *Service {
return &Service{
compiler: builder,
version: version,
playground: playground,
log: zap.S().Named("langserver"),
index: analyzer.BuildPackageIndex(packages),
limiter: rate.NewLimiter(rate.Every(frameTime), compileRequestsPerFrame),
compiler: builder,
version: version,
playgrounds: playgrounds,
log: zap.S().Named("langserver"),
index: analyzer.BuildPackageIndex(packages),
limiter: rate.NewLimiter(rate.Every(frameTime), compileRequestsPerFrame),
}
}

Expand Down Expand Up @@ -122,7 +130,7 @@ func (s *Service) provideSuggestion(req SuggestionRequest) (*SuggestionsResponse
}

// HandleGetVersion handles /api/version
func (s *Service) HandleGetVersion(w http.ResponseWriter, r *http.Request) error {
func (s *Service) HandleGetVersion(w http.ResponseWriter, _ *http.Request) error {
WriteJSON(w, VersionResponse{Version: s.version})
return nil
}
Expand All @@ -149,7 +157,7 @@ func (s *Service) HandleFormatCode(w http.ResponseWriter, r *http.Request) error
return err
}

formatted, _, err := s.goImportsCode(r.Context(), src)
formatted, _, err := s.goImportsCode(r, src)
if err != nil {
if goplay.IsCompileError(err) {
return NewHTTPError(http.StatusBadRequest, err)
Expand All @@ -165,7 +173,8 @@ func (s *Service) HandleFormatCode(w http.ResponseWriter, r *http.Request) error

// HandleShare handles snippet share
func (s *Service) HandleShare(w http.ResponseWriter, r *http.Request) error {
shareID, err := s.playground.Share(r.Context(), r.Body)
client := s.getPlaygroundClientFromRequest(r)
shareID, err := client.Share(r.Context(), r.Body)
defer r.Body.Close()
if err != nil {
if err == goplay.ErrSnippetTooLarge {
Expand All @@ -184,7 +193,8 @@ func (s *Service) HandleShare(w http.ResponseWriter, r *http.Request) error {
func (s *Service) HandleGetSnippet(w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
snippetID := vars["id"]
snippet, err := s.playground.GetSnippet(r.Context(), snippetID)
client := s.getPlaygroundClientFromRequest(r)
snippet, err := client.GetSnippet(r.Context(), snippetID)
if err != nil {
if err == goplay.ErrSnippetNotFound {
return Errorf(http.StatusNotFound, "snippet %q not found", snippetID)
Expand Down Expand Up @@ -218,7 +228,7 @@ func (s *Service) HandleRunCode(w http.ResponseWriter, r *http.Request) error {

var changed bool
if shouldFormat {
src, changed, err = s.goImportsCode(r.Context(), src)
src, changed, err = s.goImportsCode(r, src)
if err != nil {
if goplay.IsCompileError(err) {
return NewHTTPError(http.StatusBadRequest, err)
Expand All @@ -228,7 +238,8 @@ func (s *Service) HandleRunCode(w http.ResponseWriter, r *http.Request) error {
}
}

res, err := s.playground.Compile(r.Context(), src)
client := s.getPlaygroundClientFromRequest(r)
res, err := client.Compile(r.Context(), src)
if err != nil {
return err
}
Expand Down Expand Up @@ -276,6 +287,16 @@ func (s *Service) HandleArtifactRequest(w http.ResponseWriter, r *http.Request)
return nil
}

func (s *Service) getPlaygroundClientFromRequest(r *http.Request) *goplay.Client {
playgroundBackend := strings.TrimSpace(r.URL.Query().Get(playgroundBackendParam))
if playgroundBackend == playgroundGoTip {
s.log.Debugw("Using goTip backend for request", zap.String("url", r.RequestURI))
return s.playgrounds.GoTip
}

return s.playgrounds.Default
}

// HandleCompile handles WASM build request
func (s *Service) HandleCompile(w http.ResponseWriter, r *http.Request) error {
// Limit for request timeout
Expand All @@ -299,7 +320,7 @@ func (s *Service) HandleCompile(w http.ResponseWriter, r *http.Request) error {

var changed bool
if shouldFormat {
src, changed, err = s.goImportsCode(r.Context(), src)
src, changed, err = s.goImportsCode(r, src)
if err != nil {
if goplay.IsCompileError(err) {
return NewHTTPError(http.StatusBadRequest, err)
Expand Down Expand Up @@ -333,8 +354,9 @@ func (s *Service) HandleCompile(w http.ResponseWriter, r *http.Request) error {
// if any error occurs, it sends error response to client and closes connection
//
// if "format" url query param is undefined or set to "false", just returns code as is
func (s *Service) goImportsCode(ctx context.Context, src []byte) ([]byte, bool, error) {
resp, err := s.playground.GoImports(ctx, src)
func (s *Service) goImportsCode(r *http.Request, src []byte) ([]byte, bool, error) {
client := s.getPlaygroundClientFromRequest(r)
resp, err := client.GoImports(r.Context(), src)
if err != nil {
if err == goplay.ErrSnippetTooLarge {
return nil, false, NewHTTPError(http.StatusRequestEntityTooLarge, err)
Expand Down
72 changes: 49 additions & 23 deletions web/src/components/settings/SettingsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
import React from 'react';
import { Checkbox, Dropdown, getTheme, IconButton, IDropdownOption, Modal } from '@fluentui/react';
import { Pivot, PivotItem } from '@fluentui/react/lib/Pivot';
import { MessageBar, MessageBarType } from '@fluentui/react/lib/MessageBar';
import { Link } from '@fluentui/react/lib/Link';
import {
Checkbox,
Dropdown,
getTheme,
IconButton,
IDropdownOption,
Modal
} from '@fluentui/react';
import {Pivot, PivotItem} from '@fluentui/react/lib/Pivot';
import {MessageBar, MessageBarType} from '@fluentui/react/lib/MessageBar';
import {Link} from '@fluentui/react/lib/Link';

import { getContentStyles, getIconButtonStyles } from '~/styles/modal';
import {getContentStyles, getIconButtonStyles} from '~/styles/modal';
import SettingsProperty from './SettingsProperty';
import { MonacoSettings, RuntimeType } from '~/services/config';
import { DEFAULT_FONT, getAvailableFonts } from '~/services/fonts';
import { BuildParamsArgs, Connect, MonacoParamsChanges, SettingsState } from '~/store';
import {MonacoSettings, RuntimeType} from '~/services/config';
import {DEFAULT_FONT, getAvailableFonts} from '~/services/fonts';
import {
BuildParamsArgs,
Connect,
MonacoParamsChanges,
SettingsState
} from '~/store';

const WASM_SUPPORTED = 'WebAssembly' in window;

const COMPILER_OPTIONS: IDropdownOption[] = [
{ key: RuntimeType.GoPlayground, text: 'Go Playground' },
{
key: RuntimeType.GoTipPlayground, text: 'Go Playground (Go Tip)' },
{
key: RuntimeType.WebAssembly,
text: `WebAssembly (${WASM_SUPPORTED ? 'Experimental' : 'Unsupported'})`,
Expand Down Expand Up @@ -62,6 +76,7 @@ export interface SettingsProps {
interface SettingsModalState {
isOpen?: boolean,
showWarning?: boolean
showGoTipMessage?: boolean
}

@Connect(state => ({
Expand All @@ -77,7 +92,8 @@ export default class SettingsModal extends React.Component<SettingsProps, Settin
super(props);
this.state = {
isOpen: props.isOpen,
showWarning: props.settings.runtime === RuntimeType.WebAssembly
showWarning: props.settings.runtime === RuntimeType.WebAssembly,
showGoTipMessage: props.settings.runtime === RuntimeType.GoTipPlayground,
}
}

Expand All @@ -94,14 +110,11 @@ export default class SettingsModal extends React.Component<SettingsProps, Settin
this.changes.monaco[key] = val;
}

get wasmWarningVisibility() {
return this.state.showWarning ? 'visible' : 'hidden'
}

render() {
const theme = getTheme();
const contentStyles = getContentStyles(theme);
const iconButtonStyles = getIconButtonStyles(theme);
const { showGoTipMessage, showWarning } = this.state;
return (
<Modal
titleAriaId={this.titleID}
Expand Down Expand Up @@ -242,19 +255,32 @@ export default class SettingsModal extends React.Component<SettingsProps, Settin
autoFormat: this.props.settings?.autoFormat ?? true,
};

this.setState({ showWarning: val?.key === RuntimeType.WebAssembly });
this.setState({
showWarning: val?.key === RuntimeType.WebAssembly,
showGoTipMessage: val?.key === RuntimeType.GoTipPlayground
});
}}
/>}
/>
<div style={{ visibility: this.wasmWarningVisibility, marginTop: '10px' }}>
<MessageBar isMultiline={true} messageBarType={MessageBarType.warning}>
<b>WebAssembly</b> is a modern runtime that gives you additional features
like possibility to interact with web browser but is unstable.
Use it at your own risk.
<p>
See<Link href='https://github.com/golang/go/wiki/WebAssembly' target='_blank'>documentation</Link> for more details.
</p>
</MessageBar>
<div style={{ marginTop: '10px' }}>
{ showWarning && (
<MessageBar isMultiline={true} messageBarType={MessageBarType.warning}>
<b>WebAssembly</b> is a modern runtime that gives you additional features
like possibility to interact with web browser but is unstable.
Use it at your own risk.
<p>
See<Link href='https://github.com/golang/go/wiki/WebAssembly' target='_blank'>documentation</Link> for more details.
</p>
</MessageBar>
)}
{ showGoTipMessage && (
<MessageBar isMultiline={true} messageBarType={MessageBarType.warning}>
<b>Gotip Playground</b> uses the current unstable development build of Go.
<p>
See<Link href='https://pkg.go.dev/golang.org/dl/gotip' target='_blank'>gotip help</Link> for more details.
</p>
</MessageBar>
)}
</div>
<SettingsProperty
key='autoFormat'
Expand Down
13 changes: 9 additions & 4 deletions web/src/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import config from './config';
const apiAddress = `${config.serverUrl}/api`;
const axiosClient = axios.default.create({ baseURL: apiAddress });

export enum PlaygroundBackend {
Default = '',
GoTip = 'gotip'
}

export enum EvalEventKind {
Stdout = 'stdout',
Stderr = 'stderr'
Expand Down Expand Up @@ -100,12 +105,12 @@ class Client implements IAPIClient {
return resp;
}

async evaluateCode(code: string, format: boolean): Promise<RunResponse> {
return this.post<RunResponse>(`/run?format=${Boolean(format)}`, code);
async evaluateCode(code: string, format: boolean, backend = PlaygroundBackend.Default): Promise<RunResponse> {
return this.post<RunResponse>(`/run?format=${Boolean(format)}&backend=${backend}`, code);
}

async formatCode(code: string): Promise<RunResponse> {
return this.post<RunResponse>('/format', code);
async formatCode(code: string, backend = PlaygroundBackend.Default): Promise<RunResponse> {
return this.post<RunResponse>(`/format?backend=${backend}`, code);
}

async getSnippet(id: string): Promise<Snippet> {
Expand Down
Loading