aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--backend/api/generated.go90
-rw-r--r--backend/game/hub.go7
-rw-r--r--backend/game/message.go12
-rw-r--r--frontend/app/.server/api/schema.d.ts11
-rw-r--r--frontend/app/components/GolfPlayApp.client.tsx9
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx27
-rw-r--r--openapi.yaml20
-rw-r--r--worker/Dockerfile16
-rw-r--r--worker/Makefile4
-rw-r--r--worker/exec.go186
-rw-r--r--worker/go.mod20
-rw-r--r--worker/go.sum39
-rw-r--r--worker/handlers.go67
-rw-r--r--worker/main.go64
-rw-r--r--worker/models.go84
15 files changed, 577 insertions, 79 deletions
diff --git a/backend/api/generated.go b/backend/api/generated.go
index bb9b624..f7da9ee 100644
--- a/backend/api/generated.go
+++ b/backend/api/generated.go
@@ -103,6 +103,17 @@ type GamePlayerMessageC2SReady struct {
Type string `json:"type"`
}
+// GamePlayerMessageC2SSubmit defines model for GamePlayerMessageC2SSubmit.
+type GamePlayerMessageC2SSubmit struct {
+ Data GamePlayerMessageC2SSubmitPayload `json:"data"`
+ Type string `json:"type"`
+}
+
+// GamePlayerMessageC2SSubmitPayload defines model for GamePlayerMessageC2SSubmitPayload.
+type GamePlayerMessageC2SSubmitPayload struct {
+ Code string `json:"code"`
+}
+
// GamePlayerMessageS2C defines model for GamePlayerMessageS2C.
type GamePlayerMessageS2C struct {
union json.RawMessage
@@ -429,6 +440,32 @@ func (t *GamePlayerMessageC2S) MergeGamePlayerMessageC2SCode(v GamePlayerMessage
return err
}
+// AsGamePlayerMessageC2SSubmit returns the union data inside the GamePlayerMessageC2S as a GamePlayerMessageC2SSubmit
+func (t GamePlayerMessageC2S) AsGamePlayerMessageC2SSubmit() (GamePlayerMessageC2SSubmit, error) {
+ var body GamePlayerMessageC2SSubmit
+ err := json.Unmarshal(t.union, &body)
+ return body, err
+}
+
+// FromGamePlayerMessageC2SSubmit overwrites any union data inside the GamePlayerMessageC2S as the provided GamePlayerMessageC2SSubmit
+func (t *GamePlayerMessageC2S) FromGamePlayerMessageC2SSubmit(v GamePlayerMessageC2SSubmit) error {
+ b, err := json.Marshal(v)
+ t.union = b
+ return err
+}
+
+// MergeGamePlayerMessageC2SSubmit performs a merge with any union data inside the GamePlayerMessageC2S, using the provided GamePlayerMessageC2SSubmit
+func (t *GamePlayerMessageC2S) MergeGamePlayerMessageC2SSubmit(v GamePlayerMessageC2SSubmit) error {
+ b, err := json.Marshal(v)
+ if err != nil {
+ return err
+ }
+
+ merged, err := runtime.JSONMerge(t.union, b)
+ t.union = merged
+ return err
+}
+
func (t GamePlayerMessageC2S) MarshalJSON() ([]byte, error) {
b, err := t.union.MarshalJSON()
return b, err
@@ -1549,32 +1586,33 @@ func (sh *strictHandler) GetToken(ctx echo.Context, params GetTokenParams) error
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
- "H4sIAAAAAAAC/+xZX2/bNhD/Kho3oBugxY4TFJ3f0qzNOnSd0TTYQxEYtHS2mUmkSlJNvELffSCpP6ZF",
- "W7SjpunWPAS2yLv78e53vPPpE4pYmjEKVAo0/oQyzHEKErj+tgQcA5/iXC4ZJ/9gSRhVzwlF43IRhYji",
- "FNAYnVm7QsThQ044xGgseQ4hEtESUqzE5SpTAkJyQheoKEKUYbmcLnAKUxLXBtTDRn216qGYUAkL4KhQ",
- "qjmIjFEB+kDPcfwWPuQgpPoWMSqB6o84yxISaeiDG2FO2ej9gcMcjdH3g8ZZA7MqBi84Z6WpGETESWa8",
- "pGwFvDRWhOgl4zMSx0A/v+XGVBGiN0y+ZDmNP7/ZN0wGc22qCNEVrVgDD2DasqaWSwml0AgpbnOWAZfE",
- "UCEFIfAC1Ee4w2mWKOa8oh9xQpq4hQ6uNvR7Xyu5rjey2Q1EOuAXmrebZmMisgSvprRcbWyr/cFx22SI",
- "4pxrb00FRIzGwpI7eToMW8QP0Voy1VuPXRszzmYJpF2un5TblG8l5hLiKZaW9l9Onz59dvps6IQjJJbm",
- "vDRPleeihAlQ2XyLiSR0MQUqufJR80TbQQohZJgDKi0rp+jzmQ9zQolYQqxi0DizVr87fs2lYgCGdnwc",
- "rt8W6UmCV8D/aEjFKPw5R+P3u93aEr0cnaMi3FPofHSJimsXErVyOJjz0eULKvnqIERvAceHSZ6zGLaf",
- "R6+28wrLzutjm7YJXiUM62vLxFZfV6pGoExvH0cjMY6U3S5C6dXQoPGiygaE1rmi8rQNtTNOqPzxyW+Q",
- "JCwMbhlP4u+e/NSJTCvyhWSi3gKzwzugJbzc4wvCEGgfEFxL9AdCJeO9UnlSXl17Z8Hl6PxSX3+HSL64",
- "g+gtiDyRW7LI3tNPLlk6uzNKjKIx3EHEDYbe88oJp3VSETFup9exql80TxI8U19Nh+muZ7lYL2gijyIQ",
- "wi5D1cOu45XqwhKQ7wkrevUWwVKhX/iautx/7DaAtA64b9eyAakS94VjcrE3N2t1fk6umqD+XWyBaGeG",
- "Wt2jxWsT2ohvQ/MXltHywIbJltUd07VT7d73d0vc/xJuiZou5hBJ1/3tVn8wI53qdjDy1uzXlOytEdoJ",
- "osdOKCwTyuP30OY9UcuFuxuoXTHsL0heBXY9VD1XWA9A7Zva1/Xhl6vGSkMMnNv02rKP5faliCz+dfp5",
- "nVIbZb9WX+PxDsQ9C5RbnyfJ+itRu2E8aI2aNA3GhkvXB0/rNHi3JCIgIsBB1V24LiKz5JUOkshk48Yr",
- "UbnGRO4Ox/DMaLKHZq5DXwng+4ysfmdLGvzKwHVSEjE61SNcS2RAUrwAMbhhS3p0ky2comKK45TY/p3j",
- "RDTJP2MsAawHnLlwXC+jE5dH1db2KRSUTn9WVtaUtIZFNe62b5U6QudM/4A1cUVnyQxLzoQIFEROcRLc",
- "wiw4m7xCIfoIXJjZ5vDo+Gio0LMMKM4IGqOTo+HREJmxuQ7RQNsdLHBqQrYAnRQqinp89SpW9tSeC5AX",
- "eldozfq39EbNloHzXUBxvTFgHw2He017babV+ImEVPhcXM3dhDDneOWc8IktAbFnyK+JkAGbB0aiCNHp",
- "8HgbhPrMA3vyrIROuoXWBvSqpuRpivmqglDaL0IrqoNP5ayy8IpvT+ENO+WsNzefgQ5+JHAE3SvmZ9rb",
- "DxZsJXHaLVG/srHZcQEywDXgLN9GgUn+xSmg3548Z2Z8d2D0H+pNyfaS7NV4drwK8WteH8mrEUe+2C9b",
- "i1Z+n7Z6InRp2ux5niSrIM9iLKtsGXZzf+0N7deRlVf6gHViNte26hO6i/GV3vUYi3GN36sY69axqxgb",
- "lfsUYyPx5YoxTpIKg4rs7gbrW2/1FfVWvl3Vt4bqf9FQKUokbGF+dGZMOJgwYUK+1lv6anEyLMQt4/HG",
- "WLN8ejw6cfU49/wRSysyl6avDyr792GhZH/DxuzkTv0drf3vHidpJT6UtNoRRTegUkGtGLc3Te3yL4AH",
- "hjiaQ/Xhtl0m7/SGx1gh/lNxMbktlozLnxPyEeIAa3OBAVgURfFvAAAA//89QklP/CgAAA==",
+ "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",
+ "wKIoin8DAAD//5oSt8+jKgAA",
}
// GetSwagger returns the content of the embedded swagger specification file
diff --git a/backend/game/hub.go b/backend/game/hub.go
index c73009b..239f5da 100644
--- a/backend/game/hub.go
+++ b/backend/game/hub.go
@@ -170,8 +170,13 @@ func (hub *gameHub) run() {
},
}
}
+ case *playerMessageC2SSubmit:
+ // TODO: assert game state is gaming
+ log.Printf("submit: %v", message.message)
+ // code := msg.Data.Code
+ // TODO
default:
- log.Fatalf("unexpected message type: %T", message.message)
+ log.Printf("unexpected message type: %T", message.message)
}
case <-ticker.C:
if hub.game.state == gameStateStarting {
diff --git a/backend/game/message.go b/backend/game/message.go
index 0b413d6..d4007a3 100644
--- a/backend/game/message.go
+++ b/backend/game/message.go
@@ -14,6 +14,7 @@ const (
playerMessageTypeC2SEntry = "player:c2s:entry"
playerMessageTypeC2SReady = "player:c2s:ready"
playerMessageTypeC2SCode = "player:c2s:code"
+ playerMessageTypeC2SSubmit = "player:c2s:submit"
)
type playerMessageC2SWithClient struct {
@@ -36,6 +37,8 @@ type playerMessageC2SEntry = api.GamePlayerMessageC2SEntry
type playerMessageC2SReady = api.GamePlayerMessageC2SReady
type playerMessageC2SCode = api.GamePlayerMessageC2SCode
type playerMessageC2SCodePayload = api.GamePlayerMessageC2SCodePayload
+type playerMessageC2SSubmit = api.GamePlayerMessageC2SSubmit
+type playerMessageC2SSubmitPayload = api.GamePlayerMessageC2SSubmitPayload
func asPlayerMessageC2S(raw map[string]json.RawMessage) (playerMessageC2S, error) {
var typ string
@@ -61,6 +64,15 @@ func asPlayerMessageC2S(raw map[string]json.RawMessage) (playerMessageC2S, error
Type: playerMessageTypeC2SCode,
Data: payload,
}, nil
+ case playerMessageTypeC2SSubmit:
+ var payload playerMessageC2SSubmitPayload
+ if err := json.Unmarshal(raw["data"], &payload); err != nil {
+ return nil, err
+ }
+ return &playerMessageC2SSubmit{
+ Type: playerMessageTypeC2SSubmit,
+ Data: payload,
+ }, nil
default:
return nil, fmt.Errorf("unknown message type: %s", typ)
}
diff --git a/frontend/app/.server/api/schema.d.ts b/frontend/app/.server/api/schema.d.ts
index 705380d..1c8cead 100644
--- a/frontend/app/.server/api/schema.d.ts
+++ b/frontend/app/.server/api/schema.d.ts
@@ -201,7 +201,7 @@ export interface components {
/** @example 100 */
score: number | null;
};
- GamePlayerMessageC2S: components["schemas"]["GamePlayerMessageC2SEntry"] | components["schemas"]["GamePlayerMessageC2SReady"] | components["schemas"]["GamePlayerMessageC2SCode"];
+ GamePlayerMessageC2S: components["schemas"]["GamePlayerMessageC2SEntry"] | components["schemas"]["GamePlayerMessageC2SReady"] | components["schemas"]["GamePlayerMessageC2SCode"] | components["schemas"]["GamePlayerMessageC2SSubmit"];
GamePlayerMessageC2SEntry: {
/** @constant */
type: "player:c2s:entry";
@@ -219,6 +219,15 @@ export interface components {
/** @example print('Hello, world!') */
code: string;
};
+ GamePlayerMessageC2SSubmit: {
+ /** @constant */
+ type: "player:c2s:submit";
+ data: components["schemas"]["GamePlayerMessageC2SSubmitPayload"];
+ };
+ GamePlayerMessageC2SSubmitPayload: {
+ /** @example print('Hello, world!') */
+ code: string;
+ };
GameWatcherMessage: components["schemas"]["GameWatcherMessageS2C"];
GameWatcherMessageS2C: components["schemas"]["GameWatcherMessageS2CStart"] | components["schemas"]["GameWatcherMessageS2CCode"] | components["schemas"]["GameWatcherMessageS2CExecResult"];
GameWatcherMessageS2CStart: {
diff --git a/frontend/app/components/GolfPlayApp.client.tsx b/frontend/app/components/GolfPlayApp.client.tsx
index ace0710..80e7182 100644
--- a/frontend/app/components/GolfPlayApp.client.tsx
+++ b/frontend/app/components/GolfPlayApp.client.tsx
@@ -81,6 +81,14 @@ export default function GolfPlayApp({
});
}, 1000);
+ const onCodeSubmit = useDebouncedCallback((code: string) => {
+ console.log("player:c2s:submit");
+ sendJsonMessage({
+ type: "player:c2s:submit",
+ data: { code },
+ });
+ }, 1000);
+
if (readyState === ReadyState.UNINSTANTIATED) {
throw new Error("WebSocket is not connected");
}
@@ -140,6 +148,7 @@ export default function GolfPlayApp({
<GolfPlayAppGaming
problem={problem!.description}
onCodeChange={onCodeChange}
+ onCodeSubmit={onCodeSubmit}
currentScore={currentScore}
/>
);
diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
index 75ab18e..9fddb01 100644
--- a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
+++ b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
@@ -1,18 +1,30 @@
+import React, { useRef } from "react";
+
type Props = {
problem: string;
onCodeChange: (code: string) => void;
+ onCodeSubmit: (code: string) => void;
currentScore: number | null;
};
export default function GolfPlayAppGaming({
problem,
onCodeChange,
+ onCodeSubmit,
currentScore,
}: Props) {
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
+
const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
onCodeChange(e.target.value);
};
+ const handleSubmitButtonClick = () => {
+ if (textareaRef.current) {
+ onCodeSubmit(textareaRef.current.value);
+ }
+ };
+
return (
<div className="min-h-screen flex">
<div className="mx-auto flex min-h-full flex-grow">
@@ -22,16 +34,25 @@ export default function GolfPlayAppGaming({
<div className="text-gray-700">{problem}</div>
</div>
<div className="mb-4 mt-auto">
- <div className="font-semibold text-green-500">
- Score: {currentScore == null ? "-" : `${currentScore}`}
+ <div className="mb-2">
+ <div className="font-semibold text-green-500">
+ Score: {currentScore == null ? "-" : `${currentScore}`}
+ </div>
</div>
+ <button
+ onClick={handleSubmitButtonClick}
+ className="focus:shadow-outline rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700 focus:outline-none"
+ >
+ Submit
+ </button>
</div>
</div>
<div className="w-1/2 p-4 flex">
<div className="flex-grow">
<textarea
- className="h-full w-full rounded-lg border border-gray-300 p-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
+ ref={textareaRef}
onChange={handleTextChange}
+ className="h-full w-full rounded-lg border border-gray-300 p-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
></textarea>
</div>
</div>
diff --git a/openapi.yaml b/openapi.yaml
index 683fadf..d04951d 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -427,6 +427,7 @@ components:
- $ref: '#/components/schemas/GamePlayerMessageC2SEntry'
- $ref: '#/components/schemas/GamePlayerMessageC2SReady'
- $ref: '#/components/schemas/GamePlayerMessageC2SCode'
+ - $ref: '#/components/schemas/GamePlayerMessageC2SSubmit'
GamePlayerMessageC2SEntry:
type: object
properties:
@@ -462,6 +463,25 @@ components:
example: "print('Hello, world!')"
required:
- code
+ GamePlayerMessageC2SSubmit:
+ type: object
+ properties:
+ type:
+ type: string
+ const: "player:c2s:submit"
+ data:
+ $ref: '#/components/schemas/GamePlayerMessageC2SSubmitPayload'
+ required:
+ - type
+ - data
+ GamePlayerMessageC2SSubmitPayload:
+ type: object
+ properties:
+ code:
+ type: string
+ example: "print('Hello, world!')"
+ required:
+ - code
GameWatcherMessage:
oneOf:
- $ref: '#/components/schemas/GameWatcherMessageS2C'
diff --git a/worker/Dockerfile b/worker/Dockerfile
index 1d1523d..2373f57 100644
--- a/worker/Dockerfile
+++ b/worker/Dockerfile
@@ -1,13 +1,21 @@
FROM golang:1.22.3 AS builder
WORKDIR /build
-COPY . /build
-RUN go build -o /build/server .
-################################################################################
-FROM golang:1.22.3
+RUN apt-get update && apt-get install -y curl xz-utils
+RUN curl https://wasmtime.dev/install.sh -sSf | bash -s -- --version v23.0.1
+
+COPY go.mod go.sum ./
+RUN go mod download
+COPY *.go /build
+RUN CGO_ENABLED=0 go build -o /build/server .
+
+# ################################################################################
+FROM ghcr.io/swiftwasm/swift:5.10-focal
WORKDIR /app
+
+COPY --from=builder /root/.wasmtime/bin/wasmtime /usr/bin/wasmtime
COPY --from=builder /build/server /app/server
CMD ["/app/server"]
diff --git a/worker/Makefile b/worker/Makefile
index 4d6cf39..3c69e4b 100644
--- a/worker/Makefile
+++ b/worker/Makefile
@@ -5,3 +5,7 @@ fmt:
.PHONY: check
check:
go build -o /dev/null ./...
+
+.PHONY: lint
+lint:
+ go vet ./...
diff --git a/worker/exec.go b/worker/exec.go
new file mode 100644
index 0000000..2ef16fa
--- /dev/null
+++ b/worker/exec.go
@@ -0,0 +1,186 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "crypto/md5"
+ "fmt"
+ "os"
+ "os/exec"
+ "strings"
+ "time"
+)
+
+const (
+ dataRootDir = "/app/data"
+ // Stores *.swift files.
+ dataSwiftRootDir = dataRootDir + "/swift"
+ // Stores *.wasm files.
+ dataWasmRootDir = dataRootDir + "/wasm"
+ // Stores *.cwasm files (compiled wasm generated by "wasmtime compile").
+ dataCwasmRootDir = dataRootDir + "/cwasm"
+
+ wasmMaxMemorySize = 10 * 1024 * 1024 // 10 MiB
+)
+
+func prepareDirectories() error {
+ if err := os.MkdirAll(dataSwiftRootDir, 0755); err != nil {
+ return err
+ }
+ if err := os.MkdirAll(dataWasmRootDir, 0755); err != nil {
+ return err
+ }
+ if err := os.MkdirAll(dataCwasmRootDir, 0755); err != nil {
+ return err
+ }
+ return nil
+}
+
+func calcHash(code string) string {
+ return fmt.Sprintf("%x", md5.Sum([]byte(code)))
+}
+
+func calcFilePath(hash, ext string) string {
+ return fmt.Sprintf("%s/%s/%s.%s", dataRootDir, ext, hash, ext)
+}
+
+func execCommandWithTimeout(
+ ctx context.Context,
+ maxDuration time.Duration,
+ makeCmd func(context.Context) *exec.Cmd,
+) (string, string, error) {
+ ctx, cancel := context.WithTimeout(ctx, maxDuration)
+ defer cancel()
+
+ cmd := makeCmd(ctx)
+
+ var stdout bytes.Buffer
+ var stderr bytes.Buffer
+ cmd.Stdout = &stdout
+ cmd.Stderr = &stderr
+
+ exitCh := make(chan error)
+ go func() {
+ exitCh <- cmd.Run()
+ }()
+
+ select {
+ case <-ctx.Done():
+ return stdout.String(), stderr.String(), ctx.Err()
+ case err := <-exitCh:
+ return stdout.String(), stderr.String(), err
+ }
+}
+
+func convertCommandErrorToResultType(err error) string {
+ if err != nil {
+ if err == context.DeadlineExceeded {
+ return resultTimeout
+ } else {
+ return resultFailure
+ }
+ } else {
+ return resultSuccess
+ }
+}
+
+func execSwiftCompile(
+ ctx context.Context,
+ code string,
+ maxDuration time.Duration,
+) swiftCompileResponseData {
+ hash := calcHash(code)
+ inPath := calcFilePath(hash, "swift")
+ outPath := calcFilePath(hash, "wasm")
+
+ if err := os.WriteFile(inPath, []byte(code), 0644); err != nil {
+ return swiftCompileResponseData{
+ Result: resultInternalError,
+ Stdout: "",
+ Stderr: err.Error(),
+ }
+ }
+
+ stdout, stderr, err := execCommandWithTimeout(
+ ctx,
+ maxDuration,
+ func(ctx context.Context) *exec.Cmd {
+ return exec.CommandContext(
+ ctx,
+ "swiftc",
+ "-target", "wasm32-unknown-wasi",
+ "-o", outPath,
+ inPath,
+ )
+ },
+ )
+
+ return swiftCompileResponseData{
+ Result: convertCommandErrorToResultType(err),
+ Stdout: stdout,
+ Stderr: stderr,
+ }
+}
+
+func execWasmCompile(
+ ctx context.Context,
+ code string,
+ maxDuration time.Duration,
+) wasmCompileResponseData {
+ hash := calcHash(code)
+ inPath := calcFilePath(hash, "wasm")
+ outPath := calcFilePath(hash, "cwasm")
+
+ stdout, stderr, err := execCommandWithTimeout(
+ ctx,
+ maxDuration,
+ func(ctx context.Context) *exec.Cmd {
+ return exec.CommandContext(
+ ctx,
+ "wasmtime", "compile",
+ "-O", "opt-level=0",
+ "-C", "cache=n",
+ "-W", fmt.Sprintf("max-memory-size=%d", wasmMaxMemorySize),
+ "-o", outPath,
+ inPath,
+ )
+ },
+ )
+
+ return wasmCompileResponseData{
+ Result: convertCommandErrorToResultType(err),
+ Stdout: stdout,
+ Stderr: stderr,
+ }
+}
+
+func execTestRun(
+ ctx context.Context,
+ code string,
+ stdin string,
+ maxDuration time.Duration,
+) testRunResponseData {
+ hash := calcHash(code)
+ inPath := calcFilePath(hash, "cwasm")
+
+ stdout, stderr, err := execCommandWithTimeout(
+ ctx,
+ maxDuration,
+ func(ctx context.Context) *exec.Cmd {
+ cmd := exec.CommandContext(
+ ctx,
+ "wasmtime", "run",
+ "--allow-precompiled",
+ inPath,
+ )
+ cmd.Stdin = strings.NewReader(stdin)
+ return cmd
+ },
+ )
+
+ return testRunResponseData{
+ Result: convertCommandErrorToResultType(err),
+ Stdout: stdout,
+ Stderr: stderr,
+ }
+}
diff --git a/worker/go.mod b/worker/go.mod
index 007435d..1d01907 100644
--- a/worker/go.mod
+++ b/worker/go.mod
@@ -1,3 +1,23 @@
module github.com/nsfisis/iosdc-japan-2024-albatross/worker
go 1.22.3
+
+require (
+ github.com/labstack/echo-jwt/v4 v4.2.0
+ github.com/labstack/echo/v4 v4.12.0
+)
+
+require (
+ github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
+ github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
+ github.com/labstack/gommon v0.4.2 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/valyala/fasttemplate v1.2.2 // indirect
+ golang.org/x/crypto v0.22.0 // indirect
+ golang.org/x/net v0.24.0 // indirect
+ golang.org/x/sys v0.19.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+ golang.org/x/time v0.5.0 // indirect
+)
diff --git a/worker/go.sum b/worker/go.sum
new file mode 100644
index 0000000..26616a1
--- /dev/null
+++ b/worker/go.sum
@@ -0,0 +1,39 @@
+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/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
+github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/labstack/echo-jwt/v4 v4.2.0 h1:odSISV9JgcSCuhgQSV/6Io3i7nUmfM/QkBeR5GVJj5c=
+github.com/labstack/echo-jwt/v4 v4.2.0/go.mod h1:MA2RqdXdEn4/uEglx0HcUOgQSyBaTh5JcaHIan3biwU=
+github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
+github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
+github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
+github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
+github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
+golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
+golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/worker/handlers.go b/worker/handlers.go
new file mode 100644
index 0000000..e7ceef6
--- /dev/null
+++ b/worker/handlers.go
@@ -0,0 +1,67 @@
+package main
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/labstack/echo/v4"
+)
+
+func newBadRequestError(err error) *echo.HTTPError {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid request: %s", err.Error()))
+}
+
+func handleSwiftCompile(c echo.Context) error {
+ var req swiftCompileRequestData
+ if err := c.Bind(&req); err != nil {
+ return newBadRequestError(err)
+ }
+ if err := req.validate(); err != nil {
+ return newBadRequestError(err)
+ }
+
+ res := execSwiftCompile(
+ c.Request().Context(),
+ req.Code,
+ req.maxDuration(),
+ )
+
+ return c.JSON(http.StatusOK, res)
+}
+
+func handleWasmCompile(c echo.Context) error {
+ var req wasmCompileRequestData
+ if err := c.Bind(&req); err != nil {
+ return newBadRequestError(err)
+ }
+ if err := req.validate(); err != nil {
+ return newBadRequestError(err)
+ }
+
+ res := execWasmCompile(
+ c.Request().Context(),
+ req.Code,
+ req.maxDuration(),
+ )
+
+ return c.JSON(http.StatusOK, res)
+}
+
+func handleTestRun(c echo.Context) error {
+ var req testRunRequestData
+ if err := c.Bind(&req); err != nil {
+ return newBadRequestError(err)
+ }
+ if err := req.validate(); err != nil {
+ return newBadRequestError(err)
+ }
+
+ res := execTestRun(
+ c.Request().Context(),
+ req.Code,
+ req.Stdin,
+ req.maxDuration(),
+ )
+
+ return c.JSON(http.StatusOK, res)
+}
diff --git a/worker/main.go b/worker/main.go
index 0a52b0e..8134a56 100644
--- a/worker/main.go
+++ b/worker/main.go
@@ -1,57 +1,33 @@
package main
import (
- "encoding/json"
+ "log"
"net/http"
- "strconv"
- "time"
-)
-
-type RequestBody struct {
- Code string `json:"code"`
- Stdin string `json:"stdin"`
-}
-type ResponseBody struct {
- Result string `json:"result"`
- Stdout string `json:"stdout"`
- Stderr string `json:"stderr"`
-}
-
-func doExec(code string, stdin string, maxDuration time.Duration) ResponseBody {
- _ = code
- _ = stdin
- _ = maxDuration
+ echojwt "github.com/labstack/echo-jwt/v4"
+ "github.com/labstack/echo/v4"
+ "github.com/labstack/echo/v4/middleware"
+)
- return ResponseBody{
- Result: "success",
- Stdout: "42",
- Stderr: "",
+func main() {
+ if err := prepareDirectories(); err != nil {
+ log.Fatal(err)
}
-}
-func execHandler(w http.ResponseWriter, r *http.Request) {
- maxDurationStr := r.URL.Query().Get("max_duration")
- maxDuration, err := strconv.Atoi(maxDurationStr)
- if err != nil || maxDuration <= 0 {
- http.Error(w, "Invalid max_duration parameter", http.StatusBadRequest)
- return
- }
+ e := echo.New()
- var reqBody RequestBody
- err = json.NewDecoder(r.Body).Decode(&reqBody)
- if err != nil {
- http.Error(w, "Invalid request body", http.StatusBadRequest)
- return
- }
+ e.Use(middleware.Logger())
+ e.Use(middleware.Recover())
- resBody := doExec(reqBody.Code, reqBody.Stdin, time.Duration(maxDuration)*time.Second)
+ e.Use(echojwt.WithConfig(echojwt.Config{
+ SigningKey: []byte("TODO"),
+ }))
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(resBody)
-}
+ e.POST("/api/swiftc", handleSwiftCompile)
+ e.POST("/api/wasmc", handleWasmCompile)
+ e.POST("/api/testrun", handleTestRun)
-func main() {
- http.HandleFunc("/api/exec", execHandler)
- http.ListenAndServe(":80", nil)
+ if err := e.Start(":80"); err != http.ErrServerClosed {
+ log.Fatal(err)
+ }
}
diff --git a/worker/models.go b/worker/models.go
new file mode 100644
index 0000000..b838fe0
--- /dev/null
+++ b/worker/models.go
@@ -0,0 +1,84 @@
+package main
+
+import (
+ "errors"
+ "time"
+)
+
+const (
+ resultSuccess = "success"
+ resultFailure = "failure"
+ resultTimeout = "timeout"
+ resultInternalError = "internal_error"
+)
+
+var (
+ errInvalidMaxDuration = errors.New("'max_duration_ms' must be positive")
+)
+
+type swiftCompileRequestData struct {
+ MaxDurationMilliseconds int `json:"max_duration_ms"`
+ Code string `json:"code"`
+}
+
+func (req *swiftCompileRequestData) maxDuration() time.Duration {
+ return time.Duration(req.MaxDurationMilliseconds) * time.Millisecond
+}
+
+func (req *swiftCompileRequestData) validate() error {
+ if req.MaxDurationMilliseconds <= 0 {
+ return errInvalidMaxDuration
+ }
+ return nil
+}
+
+type swiftCompileResponseData struct {
+ Result string `json:"result"`
+ Stdout string `json:"stdout"`
+ Stderr string `json:"stderr"`
+}
+
+type wasmCompileRequestData struct {
+ MaxDurationMilliseconds int `json:"max_duration_ms"`
+ Code string `json:"code"`
+}
+
+type wasmCompileResponseData struct {
+ Result string `json:"result"`
+ Stdout string `json:"stdout"`
+ Stderr string `json:"stderr"`
+}
+
+func (req *wasmCompileRequestData) maxDuration() time.Duration {
+ return time.Duration(req.MaxDurationMilliseconds) * time.Millisecond
+}
+
+func (req *wasmCompileRequestData) validate() error {
+ if req.MaxDurationMilliseconds <= 0 {
+ return errInvalidMaxDuration
+ }
+ return nil
+}
+
+type testRunRequestData struct {
+ MaxDurationMilliseconds int `json:"max_duration_ms"`
+ Code string `json:"code"`
+ Stdin string `json:"stdin"`
+}
+
+func (req *testRunRequestData) maxDuration() time.Duration {
+ return time.Duration(req.MaxDurationMilliseconds) * time.Millisecond
+}
+
+func (req *testRunRequestData) validate() error {
+ if req.MaxDurationMilliseconds <= 0 {
+ return errInvalidMaxDuration
+ }
+ return nil
+}
+
+type testRunResponseData struct {
+ Result string `json:"result"`
+ Stdout string `json:"stdout"`
+ Stderr string `json:"stderr"`
+}