) = statsCommand + command
16 |
17 | fun withTime(block: () -> P): Pair
{
18 | val startTime = System.nanoTime()
19 | val result = block()
20 | val endTime = System.nanoTime()
21 |
22 | return result to (endTime - startTime) / 1_000_000
23 | }
24 |
25 | fun withMemory(output: String): Pair =
26 | REGEX.toRegex().findAll(output).lastOrNull()
27 | ?.let { match ->
28 | val memory = match.groups[1]?.value?.toLongOrNull()
29 | val cleanedOutput = match.range.let { output.removeRange(it) }
30 | return Pair(cleanedOutput, memory)
31 | }
32 | ?: Pair(output, null)
33 |
34 | private fun buildStatsCall(): List {
35 | val command = when {
36 | getOsName().contains("mac") -> "gtime"
37 | else -> "/usr/bin/time"
38 | }
39 |
40 | return getStatsCliInstalled(command)
41 | ?.let { listOf(it, "-f", "[%M]") }
42 | .orEmpty()
43 | }
44 |
45 | private fun getOsName() = System.getProperty("os.name").lowercase()
46 |
47 | private fun getStatsCliInstalled(name: String): String? {
48 | return try {
49 | ProcessBuilder(listOf(name, "--h")).start().waitFor()
50 | name
51 | } catch (e: Exception) {
52 | null
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/coderunner/src/main/kotlin/io/codegeet/platform/coderunner/Runner.kt:
--------------------------------------------------------------------------------
1 | package io.codegeet.platform.coderunner
2 |
3 | import io.codegeet.platform.coderunner.exception.CompilationException
4 | import io.codegeet.platform.coderunner.exception.TimeLimitException
5 | import io.codegeet.platform.common.ExecutionRequest
6 | import io.codegeet.platform.common.ExecutionResult
7 | import io.codegeet.platform.common.ExecutionStatus
8 | import io.codegeet.platform.common.InvocationStatus
9 | import io.codegeet.platform.common.language.LanguageConfig
10 | import java.nio.file.Files
11 | import java.nio.file.Path
12 | import java.nio.file.StandardOpenOption
13 |
14 | class Runner(private val processExecutor: ProcessExecutor) {
15 |
16 | companion object {
17 | const val DEFAULT_INVOCATION_TIMEOUT_MILLIS: Long = 5_000
18 | }
19 |
20 | fun run(input: ExecutionRequest): ExecutionResult {
21 | return try {
22 | initDirectory(input)
23 | val compilationDetails = compileIfNeeded(input)
24 | val invocationResult = invoke(input)
25 | ExecutionResult(
26 | status = calculateExecutionStatus(invocationResult),
27 | compilation = compilationDetails,
28 | invocations = invocationResult,
29 | )
30 | } catch (e: CompilationException) {
31 | ExecutionResult(
32 | status = ExecutionStatus.COMPILATION_ERROR,
33 | error = e.message,
34 | )
35 | } catch (e: Exception) {
36 | ExecutionResult(
37 | status = ExecutionStatus.INTERNAL_ERROR,
38 | error = "Something went wrong during the execution: ${e.message}"
39 | )
40 | }
41 | }
42 |
43 | private fun calculateExecutionStatus(invocationResult: List) =
44 | if (invocationResult.all { it.status == InvocationStatus.SUCCESS }) ExecutionStatus.SUCCESS else ExecutionStatus.INVOCATION_ERROR
45 |
46 | private fun compileIfNeeded(input: ExecutionRequest): ExecutionResult.CompilationResult? =
47 | LanguageConfig.get(input.language).compilation?.let { command -> compile(command) }
48 |
49 | private fun invoke(input: ExecutionRequest): List =
50 | input.invocations.ifEmpty { listOf(ExecutionRequest.InvocationRequest()) }
51 | .map { invocation ->
52 | runCatching {
53 | val command = LanguageConfig.get(input.language).invocation
54 | invocation(command, invocation)
55 | }.getOrElse { e ->
56 | when (e) {
57 | is TimeLimitException -> ExecutionResult.InvocationResult(
58 | status = InvocationStatus.TIMEOUT,
59 | error = e.message
60 | )
61 |
62 | else -> ExecutionResult.InvocationResult(
63 | status = InvocationStatus.INTERNAL_ERROR,
64 | error = "Something went wrong during the invocation: ${e.message}"
65 | )
66 | }
67 | }
68 | }
69 |
70 | private fun initDirectory(input: ExecutionRequest) = try {
71 | val directory = getUserHomeDirectory()
72 | writeFiles(input.code, directory, LanguageConfig.get(input.language).fileName)
73 |
74 | directory
75 | } catch (e: Exception) {
76 | throw Exception("Something went wrong during the preparation: ${e.message}", e)
77 | }
78 |
79 | private fun compile(
80 | compilationCommand: String
81 | ): ExecutionResult.CompilationResult {
82 | try {
83 | val process = processExecutor.execute(compilationCommand.split(" "))
84 |
85 | if (!process.completed) {
86 | throw CompilationException(process.stdErr.takeIf { it.isNotEmpty() } ?: process.stdOut)
87 | }
88 |
89 | return ExecutionResult.CompilationResult(
90 | details = ExecutionResult.Details(
91 | runtime = process.time,
92 | memory = process.memory
93 | )
94 | )
95 | } catch (e: CompilationException) {
96 | throw e
97 | } catch (e: Exception) {
98 | throw Exception("Compilation failed: ${e.message}", e)
99 | }
100 | }
101 |
102 | private fun invocation(
103 | invocationCommand: String,
104 | invocation: ExecutionRequest.InvocationRequest
105 | ): ExecutionResult.InvocationResult {
106 |
107 | val process = processExecutor.execute(
108 | command = invocationCommand.split(" ") + invocation.args.orEmpty(),
109 | input = invocation.stdIn,
110 | timeout = DEFAULT_INVOCATION_TIMEOUT_MILLIS
111 | )
112 |
113 | return ExecutionResult.InvocationResult(
114 | status = if (process.completed) InvocationStatus.SUCCESS else InvocationStatus.INVOCATION_ERROR,
115 | details = ExecutionResult.Details(
116 | runtime = process.time,
117 | memory = process.memory,
118 | ),
119 | stdOut = process.stdOut,
120 | stdErr = process.stdErr,
121 | )
122 | }
123 |
124 | private fun getUserHomeDirectory(): String {
125 | return System.getProperty("user.home")
126 | }
127 |
128 | private fun writeFiles(
129 | content: String,
130 | directory: String,
131 | fileName: String
132 | ) {
133 | val path = Path.of(directory, fileName)
134 |
135 | Files.write(
136 | path,
137 | content.toByteArray(),
138 | StandardOpenOption.CREATE,
139 | StandardOpenOption.TRUNCATE_EXISTING
140 | )
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/coderunner/src/main/kotlin/io/codegeet/platform/coderunner/exception/CompilationException.kt:
--------------------------------------------------------------------------------
1 | package io.codegeet.platform.coderunner.exception
2 |
3 | class CompilationException(message: String) : Exception(message)
4 |
--------------------------------------------------------------------------------
/coderunner/src/main/kotlin/io/codegeet/platform/coderunner/exception/OutputLimitException.kt:
--------------------------------------------------------------------------------
1 | package io.codegeet.platform.coderunner.exception
2 |
3 | class OutputLimitException(bytes: Int) :
4 | Exception("Process output exceeds limit of $bytes bytes")
--------------------------------------------------------------------------------
/coderunner/src/main/kotlin/io/codegeet/platform/coderunner/exception/TimeLimitException.kt:
--------------------------------------------------------------------------------
1 | package io.codegeet.platform.coderunner.exception
2 |
3 | import java.util.concurrent.TimeUnit
4 |
5 | class TimeLimitException(timeout: Long, timeunit: TimeUnit) :
6 | Exception("Process did not finish within $timeout ${timeunit.name.lowercase()}")
--------------------------------------------------------------------------------
/coderunner/src/test/kotlin/io/codegeet/platform/coderunner/RunnerTest.kt:
--------------------------------------------------------------------------------
1 | package io.codegeet.platform.coderunner
2 |
3 | import io.codegeet.platform.common.*
4 | import io.codegeet.platform.common.language.Language
5 | import org.junit.jupiter.api.Assertions.assertEquals
6 | import org.junit.jupiter.api.Test
7 | import org.mockito.kotlin.any
8 | import org.mockito.kotlin.doAnswer
9 | import org.mockito.kotlin.mock
10 |
11 | class RunnerTest {
12 |
13 | @Suppress("UNCHECKED_CAST")
14 | private val processStats = mock {
15 | on { wrapCommand(any()) } doAnswer { it.arguments[0] as List }
16 | on { withMemory(any()) } doAnswer { Pair(it.arguments[0] as String, 100) }
17 | on { withTime(any()) } doAnswer {
18 | val block = it.arguments[0] as () -> Process
19 | val blockResult = block()
20 | Pair(blockResult, 100)
21 | }
22 | }
23 |
24 | private val runner = Runner(ProcessExecutor(processStats))
25 |
26 | @Test
27 | fun run() {
28 | assertEquals(
29 | result(
30 | status = ExecutionStatus.SUCCESS,
31 | invocation = ExecutionResult.InvocationResult(
32 | status = InvocationStatus.SUCCESS,
33 | stdOut = "",
34 | stdErr = ""
35 | )
36 | ), runner.run(
37 | executionRequest("class Main { public static void main(String[] args) { }}")
38 | )
39 | )
40 | }
41 |
42 | @Test
43 | fun `run with stdOut output`() {
44 | assertEquals(
45 | result(
46 | status = ExecutionStatus.SUCCESS,
47 | invocation = ExecutionResult.InvocationResult(
48 | status = InvocationStatus.SUCCESS,
49 | stdOut = "test",
50 | stdErr = ""
51 | )
52 | ), runner.run(
53 | executionRequest("class Main { public static void main(String[] args) { System.out.print(\"test\"); }}")
54 | )
55 | )
56 | }
57 |
58 | @Test
59 | fun `run with stdErr output`() {
60 | assertEquals(
61 | result(
62 | status = ExecutionStatus.SUCCESS,
63 | invocation = ExecutionResult.InvocationResult(
64 | status = InvocationStatus.SUCCESS,
65 | stdOut = "",
66 | stdErr = "test"
67 | )
68 | ), runner.run(
69 | executionRequest("class Main { public static void main(String[] args) { System.err.print(\"test\"); }}")
70 | )
71 | )
72 | }
73 |
74 | @Test
75 | fun `run with arguments`() {
76 | assertEquals(
77 | result(
78 | status = ExecutionStatus.SUCCESS,
79 | invocation = ExecutionResult.InvocationResult(
80 | status = InvocationStatus.SUCCESS,
81 | stdOut = "test",
82 | stdErr = ""
83 | )
84 | ), runner.run(
85 | executionRequest(
86 | "class Main { public static void main(String[] args) { System.out.print(args[0]); }}",
87 | listOf(ExecutionRequest.InvocationRequest(args = listOf("test")))
88 | )
89 | )
90 | )
91 | }
92 |
93 | @Test
94 | fun `run with stdIn`() {
95 | assertEquals(
96 | result(
97 | status = ExecutionStatus.SUCCESS,
98 | invocation = ExecutionResult.InvocationResult(
99 | status = InvocationStatus.SUCCESS,
100 | stdOut = "test",
101 | stdErr = "",
102 | )
103 | ), runner.run(
104 | executionRequest(
105 | "import java.util.Scanner; class Main { public static void main(String[] args) { System.out.print(new Scanner(System.in).nextLine()); }}",
106 | listOf(ExecutionRequest.InvocationRequest(stdIn = "test"))
107 | )
108 | )
109 | )
110 | }
111 |
112 | @Test
113 | fun `run invocation code exception`() {
114 | assertEquals(
115 | result(
116 | status = ExecutionStatus.INVOCATION_ERROR,
117 | invocation = ExecutionResult.InvocationResult(
118 | status = InvocationStatus.INVOCATION_ERROR,
119 | stdOut = "",
120 | stdErr = "Exception in thread \"main\" java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0\n\tat Main.main(Main.java:1)\n",
121 | )
122 | ), runner.run(
123 | executionRequest("class Main { public static void main(String[] args) { System.out.print(args[0]); }}")
124 | )
125 | )
126 | }
127 |
128 | @Test
129 | fun `run invocation compilation exception`() {
130 | assertEquals(
131 | result(
132 | status = ExecutionStatus.COMPILATION_ERROR,
133 | error = "Main.java:1: error: not a statement\n" +
134 | "class Main { public static void main(String[] args) { wft; }}\n" +
135 | " ^\n" +
136 | "1 error\n",
137 | ), runner.run(
138 | executionRequest("class Main { public static void main(String[] args) { wft; }}")
139 | )
140 | )
141 | }
142 |
143 | private fun result(
144 | status: ExecutionStatus,
145 | invocation: ExecutionResult.InvocationResult? = null,
146 | error: String? = null
147 | ) = ExecutionResult(
148 | status = status,
149 | invocations = invocation
150 | ?.let { listOf(invocation.copy(details = ExecutionResult.Details(100, 100))) }
151 | ?: emptyList(),
152 | error = error,
153 | compilation = if (error == null) ExecutionResult.CompilationResult(
154 | ExecutionResult.Details(
155 | 100,
156 | 100
157 | )
158 | ) else null
159 | )
160 |
161 | private fun executionRequest(code: String) = executionRequest(code, emptyList())
162 |
163 | private fun executionRequest(code: String, invocations: List) =
164 | ExecutionRequest(
165 | code = code,
166 | language = Language.JAVA,
167 | invocations = invocations
168 | )
169 | }
170 |
--------------------------------------------------------------------------------
/common/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 |
7 | io.codegeet
8 | platform
9 | 0.1.0-SNAPSHOT
10 |
11 |
12 | common
13 | jar
14 |
15 |
16 |
17 | org.jetbrains.kotlin
18 | kotlin-stdlib
19 | ${kotlin.version}
20 |
21 |
22 | com.fasterxml.jackson.module
23 | jackson-module-kotlin
24 |
25 |
26 |
27 |
28 | src/main/kotlin
29 |
30 |
31 | org.jetbrains.kotlin
32 | kotlin-maven-plugin
33 | ${kotlin.version}
34 |
35 | 17
36 |
37 |
38 |
39 |
40 | compile
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/io/codegeet/platform/common/Model.kt:
--------------------------------------------------------------------------------
1 | package io.codegeet.platform.common
2 |
3 | import io.codegeet.platform.common.language.Language
4 | import java.util.*
5 |
6 | enum class ExecutionStatus {
7 | SUCCESS, COMPILATION_ERROR, INVOCATION_ERROR, INTERNAL_ERROR, TIMEOUT
8 | }
9 |
10 | enum class InvocationStatus {
11 | INTERNAL_ERROR, INVOCATION_ERROR, TIMEOUT, SUCCESS
12 | }
13 |
14 | data class ExecutionRequest(
15 | val code: String,
16 | val language: Language,
17 | val invocations: List = listOf(InvocationRequest()),
18 | ) {
19 | data class InvocationRequest(
20 | val args: List? = null,
21 | val stdIn: String? = null,
22 | )
23 | }
24 |
25 | data class ExecutionResult(
26 | val status: ExecutionStatus,
27 | val compilation: CompilationResult? = null,
28 | val invocations: List = Collections.emptyList(),
29 | val error: String? = null,
30 | ) {
31 | data class InvocationResult(
32 | val status: InvocationStatus,
33 | val details: Details? = null,
34 | val stdOut: String? = null,
35 | val stdErr: String? = null,
36 | val error: String? = null,
37 | )
38 |
39 | data class CompilationResult(
40 | val details: Details? = null,
41 | )
42 |
43 | data class Details(
44 | val runtime: Long? = null,
45 | val memory: Long? = null
46 | )
47 | }
48 |
49 | data class ExecutionJobRequest(
50 | val executionId: String,
51 | val request: ExecutionRequest
52 | )
53 |
54 | data class ExecutionJobReply(
55 | val executionId: String,
56 | val result: ExecutionResult
57 | )
--------------------------------------------------------------------------------
/common/src/main/kotlin/io/codegeet/platform/common/language/Language.kt:
--------------------------------------------------------------------------------
1 | package io.codegeet.platform.common.language
2 |
3 | import com.fasterxml.jackson.annotation.JsonValue
4 |
5 | enum class Language(private val id: String) {
6 | CSHARP("csharp"),
7 | JAVA("java"),
8 | JS("js"),
9 | PYTHON("python"),
10 | TS("ts"),
11 | KOTLIN("kotlin"),
12 | ONESCRIPT("onescript");
13 |
14 | @JsonValue
15 | fun getId(): String = id
16 | }
--------------------------------------------------------------------------------
/common/src/main/kotlin/io/codegeet/platform/common/language/LanguageConfig.kt:
--------------------------------------------------------------------------------
1 | package io.codegeet.platform.common.language
2 |
3 | object LanguageConfig {
4 |
5 | private val config = mapOf(
6 | Language.JAVA to Config(
7 | compilation = "javac Main.java",
8 | invocation = "java Main",
9 | fileName = "Main.java"
10 | ),
11 | Language.PYTHON to Config(
12 | compilation = null,
13 | invocation = "python3 app.py",
14 | fileName = "app.py"
15 | ),
16 | Language.CSHARP to Config(
17 | compilation = "mcs -out:app.exe app.cs",
18 | invocation = "mono app.exe",
19 | fileName = "app.cs"
20 | ),
21 | Language.JS to Config(
22 | compilation = null,
23 | invocation = "node app.js",
24 | fileName = "app.js"
25 | ),
26 | Language.TS to Config(
27 | compilation = "tsc app.ts",
28 | invocation = "node app.js",
29 | fileName = "app.ts"
30 | ),
31 | Language.KOTLIN to Config(
32 | compilation = "kotlinc app.kt -include-runtime -d app.jar",
33 | invocation = "java -jar app.jar",
34 | fileName = "app.kt"
35 | ),
36 | Language.ONESCRIPT to Config(
37 | compilation = null,
38 | invocation = "oscript script.os",
39 | fileName = "script.os"
40 | )
41 | )
42 |
43 | fun get(language: Language): Config =
44 | config[language] ?: throw IllegalArgumentException("Config for $language not found")
45 |
46 | data class Config(
47 | val compilation: String?,
48 | val invocation: String,
49 | val fileName: String,
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/images/README.md:
--------------------------------------------------------------------------------
1 | # Images
2 |
3 | ## Overview
4 | **CodeGeet** executes code within Docker containers, using a separate Docker image for each supported language.
5 | Typically, each docker image is based on a standard base image for the respective language
6 | and includes coderunner for compiling and executing code.
7 |
8 | ### Pull Docker image:
9 |
10 | ```bash
11 | docker pull codegeet/python:latest
12 | ```
13 | or
14 | ```bash
15 | docker pull codegeet/java:latest
16 | ```
17 |
18 | ### Run Docker container:
19 |
20 | #### Python
21 | ```bash
22 | echo '{
23 | "code": "print(f\"Hello, {input()}!\")",
24 | "language": "python",
25 | "invocations": [
26 | {
27 | "args": [""],
28 | "std_in": "CodeGeet"
29 | }
30 | ]
31 | }' | docker run --rm -i -u codegeet -w /home/codegeet codegeet/python:latest
32 | ```
33 |
34 | #### Java
35 | ```bash
36 | echo '{
37 | "code": "class Main { public static void main(String[] args) { System.out.print(\"Hello, \" + args[0] + \"!\"); }}",
38 | "language": "java",
39 | "invocations": [
40 | {
41 | "args": ["CodeGeet"]
42 | }
43 | ]
44 | }' | docker run --rm -i -u codegeet -w /home/codegeet codegeet/java:latest
45 | ```
46 |
47 | #### Output
48 | ```json
49 | {
50 | "status" : "SUCCESS",
51 | "compilation" : {
52 | "details" : {
53 | "runtime" : 354,
54 | "memory" : 87340
55 | }
56 | },
57 | "invocations" : [ {
58 | "status" : "SUCCESS",
59 | "details" : {
60 | "runtime" : 31,
61 | "memory" : 45992
62 | },
63 | "std_out" : "Hello, CodeGeet!",
64 | "std_err" : ""
65 | } ]
66 | }
67 | ```
68 | or
69 | ```json
70 | {
71 | "status" : "INVOCATION_ERROR",
72 | "compilation" : {
73 | "details" : {
74 | "runtime" : 282,
75 | "memory" : 96196
76 | }
77 | },
78 | "invocations" : [ {
79 | "status" : "INVOCATION_ERROR",
80 | "details" : {
81 | "runtime" : 23,
82 | "memory" : 45480
83 | },
84 | "std_out" : "",
85 | "std_err" : "Exception in thread \"main\" java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 1\n\tat Main.main(Main.java:1)\nCommand exited with non-zero status 1\n"
86 | } ]
87 | }
88 | ```
--------------------------------------------------------------------------------
/images/csharp/latest/Dockerfile:
--------------------------------------------------------------------------------
1 | #FROM mono:latest
2 |
3 | FROM debian:bookworm-slim
4 |
5 | MAINTAINER Vladimir Prudnikov
6 |
7 | #
8 | ENV MONO_VERSION 6.12.0.182
9 |
10 | RUN apt-get update \
11 | && apt-get install -y --no-install-recommends gnupg dirmngr ca-certificates \
12 | && rm -rf /var/lib/apt/lists/* \
13 | && export GNUPGHOME="$(mktemp -d)" \
14 | && gpg --batch --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF \
15 | && gpg --batch --export --armor 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF > /etc/apt/trusted.gpg.d/mono.gpg.asc \
16 | && gpgconf --kill all \
17 | && rm -rf "$GNUPGHOME" \
18 | && apt-key list | grep Xamarin \
19 | && apt-get purge -y --auto-remove gnupg dirmngr
20 |
21 | RUN echo "deb https://download.mono-project.com/repo/debian stable-buster/snapshots/$MONO_VERSION main" > /etc/apt/sources.list.d/mono-official-stable.list \
22 | && apt-get update \
23 | && apt-get install -y mono-runtime
24 |
25 | RUN apt-get update \
26 | && apt-get install -y binutils curl mono-devel ca-certificates-mono fsharp mono-vbnc nuget referenceassemblies-pcl \
27 | && rm -rf /var/lib/apt/lists/* /tmp/*
28 | #
29 |
30 | RUN set -xe \
31 | && apt-get update \
32 | && apt-get install time -y --no-install-recommends \
33 | && apt-get install openjdk-17-jdk -y --no-install-recommends \
34 | && rm -rf /var/lib/apt/lists/* /tmp/*
35 |
36 | RUN groupadd codegeet
37 | RUN useradd -m -d /home/codegeet -g codegeet -s /bin/bash codegeet
38 |
39 | ADD https://github.com/codegeet/platform/releases/download/0.1.0-SNAPSHOT/coderunner.jar /home/codegeet
40 | RUN chown codegeet:codegeet /home/codegeet/coderunner.jar
41 | RUN chmod +x /home/codegeet/coderunner.jar
42 |
43 | USER codegeet
44 | WORKDIR /home/codegeet
45 |
46 | ENTRYPOINT ["java", "-jar", "coderunner.jar"]
47 |
--------------------------------------------------------------------------------
/images/csharp/test/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo '{
4 | "code": "using System; class HelloWorld { static void Main(string[] args) { Console.WriteLine(\"Hello, \" + args[0]); } }",
5 | "language": "csharp",
6 | "invocations": [
7 | {
8 | "args": ["CodeGeet"]
9 | }
10 | ]
11 | }' | docker run --rm -i -u codegeet -w /home/codegeet codegeet/csharp:latest
12 |
--------------------------------------------------------------------------------
/images/java/latest/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM openjdk:21-slim
2 |
3 | MAINTAINER Vladimir Prudnikov
4 |
5 | RUN set -xe && \
6 | apt-get update && \
7 | apt-get install time -y --no-install-recommends && \
8 | rm -rf /var/lib/apt/lists/* /tmp/*
9 |
10 | RUN groupadd codegeet && \
11 | useradd -m -d /home/codegeet -g codegeet -s /bin/bash codegeet
12 |
13 | ADD https://github.com/codegeet/platform/releases/download/0.1.0-SNAPSHOT/coderunner.jar /home/codegeet
14 |
15 | RUN chown codegeet:codegeet /home/codegeet/coderunner.jar && \
16 | chmod +x /home/codegeet/coderunner.jar
17 |
18 | USER codegeet
19 | WORKDIR /home/codegeet
20 |
21 | ENTRYPOINT ["java", "-jar", "coderunner.jar"]
22 |
--------------------------------------------------------------------------------
/images/java/test/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo '{
4 | "code": "class Main { public static void main(String[] args) { System.out.print(\"Hello, \" + args[0] + \"!\"); }}",
5 | "language": "java",
6 | "invocations": [
7 | {
8 | "args": ["CodeGeet"]
9 | }
10 | ]
11 | }' | docker run --rm -i -u codegeet -w /home/codegeet codegeet/java:latest
12 |
--------------------------------------------------------------------------------
/images/js/latest/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:slim
2 |
3 | MAINTAINER Vladimir Prudnikov
4 |
5 | RUN set -xe && \
6 | apt-get update && \
7 | apt-get install time -y --no-install-recommends && \
8 | apt-get install openjdk-17-jdk -y --no-install-recommends && \
9 | rm -rf /var/lib/apt/lists/* /tmp/*
10 |
11 | RUN groupadd codegeet && \
12 | useradd -m -d /home/codegeet -g codegeet -s /bin/bash codegeet
13 |
14 | ADD https://github.com/codegeet/platform/releases/download/0.1.0-SNAPSHOT/coderunner.jar /home/codegeet
15 |
16 | RUN chown codegeet:codegeet /home/codegeet/coderunner.jar && \
17 | chmod +x /home/codegeet/coderunner.jar
18 |
19 | USER codegeet
20 | WORKDIR /home/codegeet
21 |
22 | ENTRYPOINT ["java", "-jar", "coderunner.jar"]
23 |
--------------------------------------------------------------------------------
/images/js/test/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo '{
4 | "code": "console.log(`Hello, ${process.argv[2]}!`);",
5 | "language": "js",
6 | "invocations": [
7 | {
8 | "args": ["CodeGeet"]
9 | }
10 | ]
11 | }' | docker run --rm -i -u codegeet -w /home/codegeet codegeet/js:latest
12 |
--------------------------------------------------------------------------------
/images/kotlin/latest/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM openjdk:21-slim
2 |
3 | MAINTAINER Vladimir Prudnikov
4 |
5 | #
6 | RUN set -xe && \
7 | apt-get update && \
8 | apt-get install unzip -y --no-install-recommends && \
9 | apt-get install wget -y --no-install-recommends
10 |
11 | ENV KOTLIN_VERSION=1.9.22
12 | ENV KOTLIN_HOME=/opt/kotlin
13 |
14 | RUN wget https://github.com/JetBrains/kotlin/releases/download/v${KOTLIN_VERSION}/kotlin-compiler-${KOTLIN_VERSION}.zip -O kotlin-compiler.zip && \
15 | unzip kotlin-compiler.zip -d /opt && \
16 | rm kotlin-compiler.zip && \
17 | mv /opt/kotlinc /opt/kotlin
18 |
19 | ENV PATH="${KOTLIN_HOME}/bin:${PATH}"
20 |
21 | RUN kotlin -version
22 | #
23 |
24 | RUN apt-get update && \
25 | apt-get install time -y --no-install-recommends && \
26 | rm -rf /var/lib/apt/lists/* /tmp/*
27 |
28 | RUN groupadd codegeet && \
29 | useradd -m -d /home/codegeet -g codegeet -s /bin/bash codegeet
30 |
31 | ADD https://github.com/codegeet/platform/releases/download/0.1.0-SNAPSHOT/coderunner.jar /home/codegeet
32 |
33 | RUN chown codegeet:codegeet /home/codegeet/coderunner.jar && \
34 | chmod +x /home/codegeet/coderunner.jar
35 |
36 | USER codegeet
37 | WORKDIR /home/codegeet
38 |
39 | ENTRYPOINT ["java", "-jar", "coderunner.jar"]
40 |
--------------------------------------------------------------------------------
/images/kotlin/test/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo '{
4 | "code": "fun main() { println(\"Hello, ${readLine()}!\") }",
5 | "language": "kotlin",
6 | "invocations": [
7 | {
8 | "std_in": "CodeGeet"
9 | }
10 | ]
11 | }' | docker run --rm -i -u codegeet -w /home/codegeet codegeet/kotlin:latest
12 |
--------------------------------------------------------------------------------
/images/onescript/latest/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:latest
2 |
3 | RUN set -xe && \
4 | apt-get update && \
5 | apt-get install time -y --no-install-recommends && \
6 | apt-get install openjdk-17-jdk -y --no-install-recommends
7 |
8 | RUN set -xe && \
9 | apt-get update && \
10 | apt-get install -y wget && \
11 | apt-get -y install locales
12 |
13 | RUN wget https://github.com/EvilBeaver/OneScript/releases/download/v1.9.0/onescript-engine_1.9.0_all.deb
14 |
15 | RUN dpkg -i ./onescript-engine_1.9.0_all.deb || apt-get install -fy
16 |
17 | RUN apt-get clean && \
18 | rm -rf /var/lib/apt/lists/* /tmp/*
19 |
20 | RUN sed -i '/ru_RU.UTF-8/s/^# //g' /etc/locale.gen && \
21 | locale-gen
22 |
23 | ENV LANG=ru_RU.UTF-8
24 | ENV LANGUAGE=ru_RU:ru
25 | ENV LC_ALL=ru_RU.UTF-8
26 |
27 | RUN groupadd codegeet && \
28 | useradd -m -d /home/codegeet -g codegeet -s /bin/bash codegeet
29 |
30 | ADD https://github.com/codegeet/platform/releases/download/0.1.0-SNAPSHOT/coderunner.jar /home/codegeet
31 |
32 | RUN chown codegeet:codegeet /home/codegeet/coderunner.jar && \
33 | chmod +x /home/codegeet/coderunner.jar
34 |
35 | USER codegeet
36 | WORKDIR /home/codegeet
37 |
38 | ENTRYPOINT ["java", "-jar", "coderunner.jar"]
--------------------------------------------------------------------------------
/images/onescript/test/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo '{
4 | "code": "Message(\"Hello, \" + CommandLineArguments.Get(0) + \"!\");",
5 | "language": "onescript",
6 | "invocations": [
7 | {
8 | "args": ["CodeGeet"],
9 | "std_in": ""
10 | }
11 | ]
12 | }' | docker run --rm -i -u codegeet -w /home/codegeet codegeet/onescript:latest
13 |
--------------------------------------------------------------------------------
/images/python/latest/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3-slim
2 |
3 | MAINTAINER Vladimir Prudnikov "codegeet@gmail.com"
4 |
5 | RUN set -xe && \
6 | apt-get update && \
7 | apt-get install time -y --no-install-recommends && \
8 | apt-get install openjdk-17-jdk -y --no-install-recommends && \
9 | rm -rf /var/lib/apt/lists/* /tmp/*
10 |
11 | RUN groupadd codegeet && \
12 | useradd -m -d /home/codegeet -g codegeet -s /bin/bash codegeet
13 |
14 | ADD https://github.com/codegeet/platform/releases/download/0.1.0-SNAPSHOT/coderunner.jar /home/codegeet
15 |
16 | RUN chown codegeet:codegeet /home/codegeet/coderunner.jar && \
17 | chmod +x /home/codegeet/coderunner.jar
18 |
19 | USER codegeet
20 | WORKDIR /home/codegeet
21 |
22 | ENTRYPOINT ["java", "-jar", "coderunner.jar"]
23 |
--------------------------------------------------------------------------------
/images/python/test/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo '{
4 | "code": "print(f\"Hello, {input()}!\")",
5 | "language": "python",
6 | "invocations": [
7 | {
8 | "args": [""],
9 | "std_in": "CodeGeet"
10 | }
11 | ]
12 | }' | docker run --rm -i -u codegeet -w /home/codegeet codegeet/python:latest
13 |
--------------------------------------------------------------------------------
/images/ts/latest/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:slim
2 |
3 | MAINTAINER Vladimir Prudnikov
4 |
5 | ENV NODE_ENV=development
6 |
7 | RUN set -xe && \
8 | npm install -g typescript
9 |
10 | RUN set -xe && \
11 | apt-get update && \
12 | apt-get install time -y --no-install-recommends && \
13 | apt-get install openjdk-17-jdk -y --no-install-recommends && \
14 | rm -rf /var/lib/apt/lists/* /tmp/*
15 |
16 | RUN groupadd codegeet && \
17 | useradd -m -d /home/codegeet -g codegeet -s /bin/bash codegeet
18 |
19 | ADD https://github.com/codegeet/platform/releases/download/0.1.0-SNAPSHOT/coderunner.jar /home/codegeet
20 |
21 | RUN chown codegeet:codegeet /home/codegeet/coderunner.jar && \
22 | chmod +x /home/codegeet/coderunner.jar
23 |
24 | USER codegeet
25 | WORKDIR /home/codegeet
26 |
27 | RUN npm install --save-dev typescript @types/node
28 |
29 | ENTRYPOINT ["java", "-jar", "coderunner.jar"]
30 |
--------------------------------------------------------------------------------
/images/ts/test/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo '{
4 | "code": "console.log(`Hello, ${process.argv[2]}!`);",
5 | "language": "ts",
6 | "invocations": [
7 | {
8 | "args": ["CodeGeet"]
9 | }
10 | ]
11 | }' | docker run --rm -i -u codegeet -w /home/codegeet codegeet/ts:latest
12 |
--------------------------------------------------------------------------------
/job/README.md:
--------------------------------------------------------------------------------
1 | ## Coderunner Job
2 |
3 | **Coderunner** job has http endpoint to test docker configuration.
4 |
5 | ```bash
6 | curl -X POST http://localhost:8080/api/executions \
7 | -H "Content-Type: application/json" \
8 | -d '{
9 | "code": "class Main { public static void main(String[] args) { System.out.print(args[0]); }}",
10 | "language": "java",
11 | "invocations": [
12 | {
13 | "args": ["one"]
14 | },
15 | {
16 | "args": ["another"]
17 | }
18 | ]
19 | }'
20 | ````
21 |
--------------------------------------------------------------------------------
/job/pom.xml:
--------------------------------------------------------------------------------
1 |
3 | 4.0.0
4 |
5 |
6 | io.codegeet
7 | platform
8 | 0.1.0-SNAPSHOT
9 |
10 |
11 | job
12 | jar
13 |
14 |
15 | 1.9.22
16 |
17 |
18 |
19 |
20 | io.codegeet
21 | common
22 | ${project.version}
23 |
24 |
25 | org.springframework.boot
26 | spring-boot-starter-web
27 |
28 |
29 | org.springframework.boot
30 | spring-boot-starter-amqp
31 |
32 |
33 | org.jetbrains.kotlinx
34 | kotlinx-coroutines-core
35 | 1.8.0
36 |
37 |
38 | org.jetbrains.kotlin
39 | kotlin-reflect
40 |
41 |
42 | org.jetbrains.kotlin
43 | kotlin-stdlib
44 | ${kotlin.version}
45 |
46 |
47 | org.jetbrains.kotlin
48 | kotlin-maven-noarg
49 | ${kotlin.version}
50 |
51 |
52 | org.jetbrains.kotlin
53 | kotlin-maven-allopen
54 | ${kotlin.version}
55 |
56 |
57 | io.github.microutils
58 | kotlin-logging-jvm
59 | 3.0.5
60 |
61 |
62 | com.fasterxml.jackson.module
63 | jackson-module-kotlin
64 | 2.16.1
65 |
66 |
67 | org.springframework.boot
68 | spring-boot-devtools
69 | runtime
70 |
71 |
72 | com.github.docker-java
73 | docker-java-core
74 | 3.3.4
75 |
76 |
77 | com.github.docker-java
78 | docker-java-transport-httpclient5
79 | 3.3.4
80 |
81 |
82 | org.apache.httpcomponents.client5
83 | httpclient5
84 | 5.2.1
85 |
86 |
87 | org.springframework.boot
88 | spring-boot-starter-test
89 | test
90 |
91 |
92 | org.junit.jupiter
93 | junit-jupiter-engine
94 | test
95 |
96 |
97 | org.jetbrains.kotlin
98 | kotlin-test
99 | ${kotlin.version}
100 | test
101 |
102 |
103 |
104 |
105 | src/main/kotlin
106 |
107 |
108 | src/main/resources
109 |
110 |
111 |
112 |
113 | org.springframework.boot
114 | spring-boot-maven-plugin
115 | ${spring.boot.version}
116 |
117 |
118 |
119 | repackage
120 |
121 |
122 |
123 |
124 |
125 | org.jetbrains.kotlin
126 | kotlin-maven-plugin
127 | ${kotlin.version}
128 |
129 |
130 | compile
131 | compile
132 |
133 | compile
134 |
135 |
136 |
137 | src/main/kotlin
138 | target/generated-sources/annotations
139 |
140 |
141 |
142 |
143 | test-compile
144 | test-compile
145 |
146 | test-compile
147 |
148 |
149 |
150 |
151 | 17
152 |
153 |
154 |
155 |
156 |
--------------------------------------------------------------------------------
/job/src/main/kotlin/io/codegeet/job/Application.kt:
--------------------------------------------------------------------------------
1 | package io.codegeet.job
2 |
3 | import org.springframework.boot.autoconfigure.SpringBootApplication
4 | import org.springframework.boot.runApplication
5 |
6 | @SpringBootApplication
7 | class Application
8 |
9 | fun main(args: Array) {
10 | runApplication(*args)
11 | }
12 |
--------------------------------------------------------------------------------
/job/src/main/kotlin/io/codegeet/job/ExecutionService.kt:
--------------------------------------------------------------------------------
1 | package io.codegeet.job
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper
4 | import com.github.dockerjava.api.DockerClient
5 | import com.github.dockerjava.api.async.ResultCallback
6 | import com.github.dockerjava.api.command.PullImageResultCallback
7 | import com.github.dockerjava.api.exception.NotFoundException
8 | import com.github.dockerjava.api.model.Frame
9 | import com.github.dockerjava.api.model.HostConfig
10 | import com.github.dockerjava.api.model.StreamType
11 | import io.codegeet.job.config.DockerConfiguration.DockerConfig
12 | import io.codegeet.platform.common.ExecutionRequest
13 | import io.codegeet.platform.common.ExecutionResult
14 | import io.codegeet.platform.common.ExecutionStatus
15 | import io.codegeet.platform.common.language.Language
16 | import kotlinx.coroutines.DelicateCoroutinesApi
17 | import kotlinx.coroutines.GlobalScope
18 | import kotlinx.coroutines.launch
19 | import mu.KotlinLogging
20 | import org.springframework.stereotype.Service
21 | import java.io.PipedInputStream
22 | import java.io.PipedOutputStream
23 | import java.nio.channels.ClosedByInterruptException
24 | import java.util.concurrent.TimeUnit
25 |
26 | @Service
27 | class ExecutionService(
28 | private val dockerClient: DockerClient,
29 | private val config: DockerConfig,
30 | private val objectMapper: ObjectMapper
31 | ) {
32 | companion object {
33 | private val logger = KotlinLogging.logger {}
34 | }
35 |
36 | @OptIn(DelicateCoroutinesApi::class)
37 | fun execute(request: ExecutionRequest): ExecutionResult {
38 | return runCatching {
39 | val containerId = createContainer(getImageName(request.language))
40 |
41 | val callback = ContainerCallback(containerId)
42 |
43 | val outputStream = PipedOutputStream()
44 | val inputStream = PipedInputStream(outputStream)
45 |
46 | val attachContainerCallback = attachContainer(containerId, inputStream, callback)
47 | startContainer(containerId)
48 |
49 | outputStream.write("${objectMapper.writeValueAsString(request)}\n\n".toByteArray())
50 | outputStream.flush()
51 | outputStream.close()
52 |
53 | attachContainerCallback.awaitCompletion(config.timeoutSeconds, TimeUnit.SECONDS)
54 | attachContainerCallback.close()
55 |
56 | GlobalScope.launch {
57 | stopContainer(containerId)
58 | removeContainer(containerId)
59 | }
60 |
61 | buildExecutionResult(callback)
62 | }.getOrElse { e ->
63 | when (e) {
64 | is NotFoundException -> {
65 | GlobalScope.launch { pull(request.language) }
66 |
67 | ExecutionResult(
68 | status = ExecutionStatus.INTERNAL_ERROR,
69 | error = "Docker image ${request.language} is not found."
70 | )
71 | }
72 |
73 | else -> {
74 | ExecutionResult(
75 | status = ExecutionStatus.INTERNAL_ERROR,
76 | error = "Docker container failure ${e.message}"
77 | )
78 | }
79 | }
80 | }
81 | }
82 |
83 | private fun pull(language: Language) {
84 | dockerClient.pullImageCmd(getImageName(language)).exec(PullImageResultCallback()).awaitCompletion()
85 | }
86 |
87 | private fun attachContainer(
88 | containerId: String,
89 | containerInputStream: PipedInputStream,
90 | containerCallback: ContainerCallback
91 | ): ContainerCallback {
92 | return dockerClient.attachContainerCmd(containerId)
93 | .withStdOut(true)
94 | .withStdErr(true)
95 | .withStdIn(containerInputStream)
96 | .withFollowStream(true)
97 | .exec(containerCallback)
98 | }
99 |
100 | private fun startContainer(containerId: String) {
101 | dockerClient.startContainerCmd(containerId).exec()
102 | }
103 |
104 | private fun removeContainer(containerId: String) {
105 | dockerClient.removeContainerCmd(containerId).exec()
106 | }
107 |
108 | private fun stopContainer(containerId: String) {
109 | try {
110 | dockerClient.stopContainerCmd(containerId).exec()
111 | } catch (e: Exception) {
112 | // logger.debug("Failed to stop container: $containerId")
113 | }
114 | }
115 |
116 | private fun createContainer(image: String): String {
117 | val hostConfig = HostConfig.newHostConfig()
118 | .withMemory(config.memory)
119 | .withNetworkMode("none")
120 |
121 | return dockerClient.createContainerCmd(image)
122 | .withStdinOpen(true)
123 | .withStdInOnce(true)
124 | .withHostConfig(hostConfig)
125 | .exec()
126 | .also { response ->
127 | response.warnings.forEach { logger.warn(it) }
128 | }.id
129 | }
130 |
131 | private fun buildExecutionResult(callback: ContainerCallback): ExecutionResult = try {
132 | callback.getStdOut()
133 | .takeIf { it.isNotEmpty() }
134 | ?.let { objectMapper.readValue(it, ExecutionResult::class.java) }
135 | ?: ExecutionResult(
136 | status = ExecutionStatus.INTERNAL_ERROR,
137 | error = callback.getStdErr().takeIf { it.isNotEmpty() } ?: "No stdout from container")
138 | } catch (e: Exception) {
139 | ExecutionResult(
140 | status = ExecutionStatus.INTERNAL_ERROR,
141 | error = "Failed to parse container output: ${callback.getStdOut()}"
142 | )
143 | }
144 |
145 | private class ContainerCallback(val containerId: String) : ResultCallback.Adapter() {
146 | private val stdOutBuilder = StringBuilder()
147 | private val stdErrBuilder = StringBuilder()
148 |
149 | override fun onNext(frame: Frame) {
150 | when (frame.streamType) {
151 | StreamType.STDOUT, StreamType.RAW -> {
152 | String(frame.payload).let {
153 | stdOutBuilder.append(it)
154 | }
155 | }
156 |
157 | StreamType.STDERR -> {
158 | String(frame.payload).let {
159 | logger.error { "STDERR $containerId: $it" }
160 | stdErrBuilder.append(it)
161 | }
162 | }
163 |
164 | StreamType.STDIN -> {
165 | String(frame.payload).let {
166 | // log ?
167 | }
168 | }
169 |
170 | else -> {}
171 | }
172 | }
173 |
174 | override fun onError(throwable: Throwable) {
175 | if (throwable is ClosedByInterruptException)
176 | "Container may have been stopped by timeout".let {
177 | logger.debug { it }
178 | stdErrBuilder.append(it)
179 | }
180 | else
181 | "Error during container execution: ${throwable.message ?: "unknown"}".let {
182 | logger.debug { it }
183 | stdErrBuilder.append(it)
184 | }
185 | }
186 |
187 | fun getStdOut(): String = stdOutBuilder.toString()
188 | fun getStdErr(): String = stdErrBuilder.toString()
189 | }
190 |
191 | private fun getImageName(language: Language) = "codegeet/${language.getId()}:latest"
192 | }
193 |
--------------------------------------------------------------------------------
/job/src/main/kotlin/io/codegeet/job/backdoor/Resource.kt:
--------------------------------------------------------------------------------
1 | package io.codegeet.job.backdoor
2 |
3 | import io.codegeet.job.ExecutionService
4 | import io.codegeet.platform.common.ExecutionRequest
5 | import io.codegeet.platform.common.ExecutionResult
6 | import org.springframework.stereotype.Controller
7 | import org.springframework.web.bind.annotation.PostMapping
8 | import org.springframework.web.bind.annotation.RequestBody
9 | import org.springframework.web.bind.annotation.RequestMapping
10 | import org.springframework.web.bind.annotation.ResponseBody
11 |
12 | @Controller
13 | @RequestMapping("api/executions")
14 | class Resource(private val service: ExecutionService) {
15 |
16 | @PostMapping
17 | @ResponseBody
18 | fun post(@RequestBody request: ExecutionRequest): ExecutionResult {
19 | return service.execute(request)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/job/src/main/kotlin/io/codegeet/job/config/Configuration.kt:
--------------------------------------------------------------------------------
1 | package io.codegeet.job.config
2 |
3 | import com.fasterxml.jackson.annotation.JsonInclude
4 | import com.fasterxml.jackson.databind.ObjectMapper
5 | import com.fasterxml.jackson.databind.PropertyNamingStrategies
6 | import com.fasterxml.jackson.module.kotlin.KotlinModule
7 | import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter
8 | import org.springframework.context.annotation.Bean
9 | import org.springframework.context.annotation.Configuration
10 | import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
11 | import java.time.Clock
12 |
13 |
14 | @Configuration
15 | class Configuration() {
16 |
17 | @Bean
18 | fun objectMapperBuilder(): ObjectMapper = Jackson2ObjectMapperBuilder.json()
19 | .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
20 | .serializationInclusion(JsonInclude.Include.NON_NULL)
21 | .modulesToInstall(KotlinModule.Builder().build())
22 | .build()
23 |
24 | @Bean
25 | fun messageConverter(objectMapper: ObjectMapper): Jackson2JsonMessageConverter =
26 | Jackson2JsonMessageConverter(ObjectMapper().registerModule(KotlinModule.Builder().build()))
27 |
28 | @Bean
29 | fun clock(): Clock = Clock.systemUTC()
30 | }
31 |
--------------------------------------------------------------------------------
/job/src/main/kotlin/io/codegeet/job/config/DockerConfiguration.kt:
--------------------------------------------------------------------------------
1 | package io.codegeet.job.config
2 |
3 | import com.github.dockerjava.api.DockerClient
4 | import com.github.dockerjava.core.DefaultDockerClientConfig
5 | import com.github.dockerjava.core.DockerClientImpl
6 | import com.github.dockerjava.httpclient5.ApacheDockerHttpClient
7 | import io.codegeet.job.config.DockerConfiguration.DockerConfig
8 | import org.springframework.boot.context.properties.ConfigurationProperties
9 | import org.springframework.boot.context.properties.EnableConfigurationProperties
10 | import org.springframework.context.annotation.Bean
11 | import org.springframework.context.annotation.Configuration
12 | import java.time.Duration
13 |
14 | @Configuration
15 | @EnableConfigurationProperties(DockerConfig::class)
16 | class DockerConfiguration() {
17 |
18 | @Bean
19 | fun dockerClient(): DockerClient {
20 | val config = DefaultDockerClientConfig.createDefaultConfigBuilder().build()
21 |
22 | val dockerHttpClient = ApacheDockerHttpClient.Builder()
23 | .dockerHost(config.dockerHost)
24 | .sslConfig(config.sslConfig)
25 | .maxConnections(50)
26 | .connectionTimeout(Duration.ofSeconds(15))
27 | .responseTimeout(Duration.ofSeconds(30))
28 | .build()
29 |
30 | val dockerClient = DockerClientImpl.getInstance(config, dockerHttpClient)
31 |
32 | try {
33 | dockerClient.pingCmd().exec()
34 | } catch (e: Exception) {
35 | throw RuntimeException("Docker client initialization failed. Docker host: '${config.dockerHost}'.", e)
36 | }
37 |
38 | return dockerClient
39 | }
40 |
41 | @ConfigurationProperties(prefix = "app.docker.container")
42 | data class DockerConfig(
43 | val memory: Long,
44 | val cpuPeriod: Long,
45 | val cpuQuota: Long,
46 | val timeoutSeconds: Long,
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/job/src/main/kotlin/io/codegeet/job/config/QueueConfiguration.kt:
--------------------------------------------------------------------------------
1 | package io.codegeet.job.config
2 |
3 | import io.codegeet.job.queue.QueueService
4 | import org.springframework.amqp.core.Binding
5 | import org.springframework.amqp.core.BindingBuilder
6 | import org.springframework.amqp.core.DirectExchange
7 | import org.springframework.amqp.core.Queue
8 | import org.springframework.amqp.rabbit.connection.ConnectionFactory
9 | import org.springframework.amqp.rabbit.core.RabbitTemplate
10 | import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer
11 | import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter
12 | import org.springframework.amqp.support.converter.MessageConverter
13 | import org.springframework.beans.factory.annotation.Qualifier
14 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
15 | import org.springframework.boot.context.properties.ConfigurationProperties
16 | import org.springframework.boot.context.properties.EnableConfigurationProperties
17 | import org.springframework.context.annotation.Bean
18 | import org.springframework.context.annotation.Configuration
19 |
20 | @Configuration
21 | @ConditionalOnProperty(name = ["app.job.enabled"], havingValue = "true", matchIfMissing = true)
22 | @EnableConfigurationProperties(
23 | value = [
24 | QueueConfiguration.RequestQueueConfig::class,
25 | QueueConfiguration.ReplyQueueConfig::class
26 | ]
27 | )
28 | class QueueConfiguration {
29 |
30 | // handle requests
31 |
32 | @Bean(name = ["requestQueue"])
33 | fun requestQueue(config: RequestQueueConfig): Queue = Queue(config.name, false)
34 |
35 | @Bean
36 | fun simpleMessageListenerContainer(
37 | connectionFactory: ConnectionFactory,
38 | messageListenerAdapter: MessageListenerAdapter,
39 | config: RequestQueueConfig
40 | ) = SimpleMessageListenerContainer()
41 | .also {
42 | it.setConnectionFactory(connectionFactory)
43 | it.setQueueNames(config.name)
44 | it.setMessageListener(messageListenerAdapter)
45 | if (config.consumers != null) {
46 | it.setConcurrentConsumers(config.consumers)
47 | }
48 | }
49 |
50 | @Bean
51 | fun messageListenerAdapter(receiver: QueueService, messageConverter: MessageConverter): MessageListenerAdapter =
52 | MessageListenerAdapter(receiver, QueueService.RECEIVE_METHOD_NAME).also {
53 | it.setMessageConverter(messageConverter)
54 | }
55 |
56 | // handle replies
57 |
58 | @Bean(name = ["replyQueue"])
59 | fun replyQueue(config: ReplyQueueConfig): Queue = Queue(config.name, false)
60 |
61 | @Bean
62 | fun replyExchange(config: ReplyQueueConfig): DirectExchange = DirectExchange(config.exchange)
63 |
64 | @Bean(name = ["replyBinding"])
65 | fun replyBinding(
66 | @Qualifier("replyQueue") queue: Queue,
67 | exchange: DirectExchange,
68 | config: ReplyQueueConfig
69 | ): Binding = BindingBuilder.bind(queue).to(exchange).with(config.routingKey)
70 |
71 | @Bean
72 | fun producerTemplate(connectionFactory: ConnectionFactory, messageConverter: MessageConverter): RabbitTemplate =
73 | RabbitTemplate(connectionFactory).also { it.messageConverter = messageConverter }
74 |
75 | @ConfigurationProperties(prefix = "app.job.queue.request")
76 | data class RequestQueueConfig(
77 | val name: String,
78 | val consumers: Int? = null,
79 | )
80 |
81 | @ConfigurationProperties(prefix = "app.job.queue.reply")
82 | data class ReplyQueueConfig(
83 | val name: String,
84 | val exchange: String,
85 | val routingKey: String,
86 | )
87 | }
88 |
--------------------------------------------------------------------------------
/job/src/main/kotlin/io/codegeet/job/queue/QueueService.kt:
--------------------------------------------------------------------------------
1 | package io.codegeet.job.queue
2 |
3 | import io.codegeet.job.ExecutionService
4 | import io.codegeet.job.config.QueueConfiguration.ReplyQueueConfig
5 | import io.codegeet.platform.common.ExecutionJobReply
6 | import io.codegeet.platform.common.ExecutionJobRequest
7 | import io.codegeet.platform.common.ExecutionResult
8 | import mu.KLogging
9 | import org.springframework.amqp.rabbit.core.RabbitTemplate
10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
11 | import org.springframework.stereotype.Service
12 |
13 | @Service
14 | @ConditionalOnProperty(name = ["app.job.enabled"], havingValue = "true", matchIfMissing = true)
15 | class QueueService(
16 | private val executionService: ExecutionService,
17 | private val replyTemplate: RabbitTemplate,
18 | private val replyQueueConfig: ReplyQueueConfig
19 | ) {
20 | companion object : KLogging() {
21 | const val RECEIVE_METHOD_NAME = "receive"
22 | }
23 |
24 | fun receive(message: ExecutionJobRequest) {
25 | logger.info("Received execution request for executionId: ${message.executionId}")
26 | send(message.executionId, executionService.execute(message.request))
27 | }
28 |
29 | private fun send(executionId: String, result: ExecutionResult) {
30 | replyTemplate.convertAndSend(
31 | replyQueueConfig.exchange,
32 | replyQueueConfig.routingKey,
33 | ExecutionJobReply(executionId, result)
34 | )
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/job/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | server.port=8081
2 |
3 | logging.level.io.codegeet.job=DEBUG
4 |
5 | app.job.enabled=true
6 |
7 | app.job.queue.request.name=execution.request.queue
8 | app.job.queue.request.poll=4
9 |
10 | app.job.queue.reply.name=execution.reply.queue
11 | app.job.queue.reply.exchange=execution.exchange
12 | app.job.queue.reply.routingKey=execution.reply.routingKey
13 |
14 | app.docker.container.memory= 768000000
15 | app.docker.container.cpuPeriod = 100000
16 | app.docker.container.cpuQuota = 50000
17 | app.docker.container.timeoutSeconds = 20
--------------------------------------------------------------------------------
/job/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | %d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | io.codegeet
7 | platform
8 | 0.1.0-SNAPSHOT
9 | pom
10 |
11 |
12 | common
13 | coderunner
14 | job
15 | api
16 |
17 |
18 |
19 | 17
20 | 1.9.20
21 | 3.1.4
22 |
23 |
24 |
25 |
26 |
27 | org.springframework.boot
28 | spring-boot-dependencies
29 | ${spring.boot.version}
30 | pom
31 | import
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | org.springframework.boot
41 | spring-boot-maven-plugin
42 |
43 |
44 | org.jetbrains.kotlin
45 | kotlin-maven-plugin
46 |
47 | 17
48 |
49 | -Xjsr305=strict
50 |
51 |
52 | spring
53 | all-open
54 | no-arg
55 | jpa
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | org.jetbrains.kotlin
67 | kotlin-maven-allopen
68 | ${kotlin.version}
69 |
70 |
71 | org.jetbrains.kotlin
72 | kotlin-maven-noarg
73 | ${kotlin.version}
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/schema.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codegeet/platform/ca6a347d947e8701018a7c2ae86fd3566d064c34/schema.jpeg
--------------------------------------------------------------------------------