├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── api ├── README.md ├── pom.xml └── src │ └── main │ ├── kotlin │ └── io │ │ └── codegeet │ │ └── platform │ │ └── api │ │ ├── Application.kt │ │ ├── auth │ │ ├── AuthConfiguration.kt │ │ └── AuthInterceptor.kt │ │ ├── config │ │ └── ApplicationConfiguration.kt │ │ ├── exceptions │ │ └── CustomExceptionHandler.kt │ │ ├── executions │ │ ├── ExecutionReplyService.kt │ │ ├── ExecutionService.kt │ │ └── model │ │ │ ├── ExecutionRepository.kt │ │ │ └── Model.kt │ │ ├── job │ │ ├── JobClient.kt │ │ └── JobConfiguration.kt │ │ └── resource │ │ └── ExecutionResource.kt │ └── resources │ ├── application.properties │ └── logback.xml ├── coderunner ├── README.md ├── coderunner.sh ├── pom.xml └── src │ ├── main │ └── kotlin │ │ └── io │ │ └── codegeet │ │ └── platform │ │ └── coderunner │ │ ├── Application.kt │ │ ├── ProcessExecutor.kt │ │ ├── ProcessStats.kt │ │ ├── Runner.kt │ │ └── exception │ │ ├── CompilationException.kt │ │ ├── OutputLimitException.kt │ │ └── TimeLimitException.kt │ └── test │ └── kotlin │ └── io │ └── codegeet │ └── platform │ └── coderunner │ └── RunnerTest.kt ├── common ├── pom.xml └── src │ └── main │ └── kotlin │ └── io │ └── codegeet │ └── platform │ └── common │ ├── Model.kt │ └── language │ ├── Language.kt │ └── LanguageConfig.kt ├── images ├── README.md ├── csharp │ ├── latest │ │ └── Dockerfile │ └── test │ │ └── test.sh ├── java │ ├── latest │ │ └── Dockerfile │ └── test │ │ └── test.sh ├── js │ ├── latest │ │ └── Dockerfile │ └── test │ │ └── test.sh ├── kotlin │ ├── latest │ │ └── Dockerfile │ └── test │ │ └── test.sh ├── onescript │ ├── latest │ │ └── Dockerfile │ └── test │ │ └── test.sh ├── python │ ├── latest │ │ └── Dockerfile │ └── test │ │ └── test.sh └── ts │ ├── latest │ └── Dockerfile │ └── test │ └── test.sh ├── job ├── README.md ├── pom.xml └── src │ └── main │ ├── kotlin │ └── io │ │ └── codegeet │ │ └── job │ │ ├── Application.kt │ │ ├── ExecutionService.kt │ │ ├── backdoor │ │ └── Resource.kt │ │ ├── config │ │ ├── Configuration.kt │ │ ├── DockerConfiguration.kt │ │ └── QueueConfiguration.kt │ │ └── queue │ │ └── QueueService.kt │ └── resources │ ├── application.properties │ └── logback.xml ├── pom.xml └── schema.jpeg /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Java CI with Maven 10 | 11 | on: 12 | workflow_dispatch: 13 | 14 | jobs: 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up JDK 17 22 | uses: actions/setup-java@v3 23 | with: 24 | java-version: '17' 25 | distribution: 'temurin' 26 | cache: maven 27 | - name: Build with Maven 28 | run: mvn -B package --file pom.xml 29 | 30 | # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive 31 | - name: Update dependency graph 32 | uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Release 0.1.0-SNAPSHOT 10 | 11 | on: 12 | workflow_dispatch: 13 | 14 | jobs: 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up JDK 17 22 | uses: actions/setup-java@v3 23 | with: 24 | java-version: '17' 25 | distribution: 'temurin' 26 | cache: maven 27 | - name: Build with Maven 28 | run: mvn -B package --file pom.xml 29 | 30 | - name: Upload Platform artifact 31 | uses: actions/upload-artifact@v3 32 | with: 33 | name: platform-artifacts 34 | path: target/platform-0.1.0-SNAPSHOT.jar 35 | 36 | - name: Upload Coderunner artifact 37 | uses: actions/upload-artifact@v3 38 | with: 39 | name: coderunner-script 40 | path: coderunner/coderunner.sh 41 | 42 | release: 43 | needs: build 44 | runs-on: ubuntu-latest 45 | steps: 46 | - name: Download Platform artifact 47 | uses: actions/download-artifact@v3 48 | with: 49 | name: platform-artifacts 50 | 51 | - name: Download Coderunner artifact 52 | uses: actions/download-artifact@v3 53 | with: 54 | name: coderunner-script 55 | 56 | - name: Create Release 57 | id: create_release 58 | uses: actions/create-release@v1 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | with: 62 | tag_name: 0.1.0-SNAPSHOT 63 | release_name: Release 0.1.0-SNAPSHOT 64 | draft: false 65 | prerelease: false 66 | 67 | - name: Upload Jar Release Asset 68 | uses: actions/upload-release-asset@v1 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | with: 72 | upload_url: ${{ steps.create_release.outputs.upload_url }} 73 | asset_path: ./platform-0.1.0-SNAPSHOT.jar 74 | asset_name: platform-0.1.0-SNAPSHOT.jar 75 | asset_content_type: application/java-archive 76 | 77 | - name: Upload Coderunner Release Asset 78 | uses: actions/upload-release-asset@v1 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | with: 82 | upload_url: ${{ steps.create_release.outputs.upload_url }} 83 | asset_path: ./coderunner.sh 84 | asset_name: coderunner.sh 85 | asset_content_type: text/x-sh 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 11 | .mvn/wrapper/maven-wrapper.jar 12 | 13 | # Eclipse m2e generated files 14 | # Eclipse Core 15 | .project 16 | # JDT-specific (Eclipse Java Development Tools) 17 | .classpath 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 CodeGeet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # CodeGeet 3 | 4 | Codegeet is an open-source solution for executing code in isolated, secure Docker containers. 5 | The platform is designed to run code snippets in a variety of programming languages for code testing, development, and educational purposes. 6 | 7 | Here you can find a [demo](https://codegeet.io/). 8 | 9 | ## Overview 10 | 11 | ![schema.jpeg](schema.jpeg) 12 | 13 | ### Components 14 | 15 | - [API Service](https://github.com/codegeet/platform/tree/main/api) that handles incoming requests, puts them into a queue, and stores them in the database 16 | - [Job Service](https://github.com/codegeet/platform/tree/main/job) reads from the queue and triggers Docker images depending on the language 17 | - [Docker Images](https://github.com/codegeet/platform/tree/main/images) for various programming languages 18 | - [Code Runner](https://github.com/codegeet/platform/tree/main/coderunner) CLI that compiles and executes the code inside a container 19 | 20 | # Interface 21 | 22 | `POST api/execution` 23 | 24 | ```json 25 | { 26 | "code": "class Main { public static void main(String[] args) { System.out.print(args[0]); }}", 27 | "language": "java", 28 | "invocations": [ 29 | { 30 | "args": ["one"] 31 | }, 32 | { 33 | "args": ["another"] 34 | } 35 | ] 36 | } 37 | ``` 38 | ```json 39 | { 40 | "execution_id": "e329581f-586a-40fd-adb0-a101e53bb770" 41 | } 42 | ``` 43 | 44 | `GET api/execution/{execution_id}` 45 | 46 | ```json 47 | { 48 | "execution_id": "ffaffae9-c71d-4412-a132-350581454958", 49 | "status": "SUCCESS", 50 | "invocations": [ 51 | { 52 | "status": "SUCCESS", 53 | "details": { 54 | "runtime": 24, 55 | "memory": 38988 56 | }, 57 | "std_out": "one", 58 | "std_err": "" 59 | }, 60 | { 61 | "status": "SUCCESS", 62 | "details": { 63 | "runtime": 24, 64 | "memory": 39132 65 | }, 66 | "std_out": "another", 67 | "std_err": "" 68 | } 69 | ] 70 | } 71 | ``` 72 | or 73 | ```json 74 | { 75 | "execution_id": "e329581f-586a-40fd-adb0-a101e53bb770", 76 | "status": "INVOCATION_ERROR", 77 | "invocations": [ 78 | { 79 | "status": "INVOCATION_ERROR", 80 | "details": { 81 | "runtime": 24, 82 | "memory": 39340 83 | }, 84 | "std_out": "", 85 | "std_err": "Exception in thread \"main\" java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0\n\tat Main.main(Main.java:1)\nCommand exited with non-zero status 1\n" 86 | } 87 | ] 88 | } 89 | ``` 90 | ## Coderunner 91 | 92 | See [coderunner](https://github.com/codegeet/codegeet/tree/main/coderunner) 93 | 94 | ## Also 95 | Inspired by 96 | - [Judge0](https://github.com/judge0) 97 | - [glot](https://github.com/glotcode) 98 | 99 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | tbd -------------------------------------------------------------------------------- /api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | io.codegeet 8 | platform 9 | 0.1.0-SNAPSHOT 10 | 11 | 12 | api 13 | jar 14 | 15 | 16 | 17 | io.codegeet 18 | common 19 | ${project.version} 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-web 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-data-mongodb 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-amqp 32 | 33 | 34 | 35 | org.jetbrains.kotlin 36 | kotlin-reflect 37 | 38 | 39 | org.jetbrains.kotlin 40 | kotlin-stdlib 41 | ${kotlin.version} 42 | 43 | 44 | org.jetbrains.kotlin 45 | kotlin-maven-noarg 46 | ${kotlin.version} 47 | 48 | 49 | org.jetbrains.kotlin 50 | kotlin-maven-allopen 51 | ${kotlin.version} 52 | 53 | 54 | com.fasterxml.jackson.module 55 | jackson-module-kotlin 56 | 2.15.2 57 | 58 | 59 | io.github.microutils 60 | kotlin-logging-jvm 61 | 3.0.5 62 | 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-devtools 67 | runtime 68 | 69 | 70 | 71 | org.springframework.boot 72 | spring-boot-starter-test 73 | test 74 | 75 | 76 | org.junit.jupiter 77 | junit-jupiter-engine 78 | test 79 | 80 | 81 | org.jetbrains.kotlin 82 | kotlin-test 83 | ${kotlin.version} 84 | test 85 | 86 | 87 | 88 | 89 | src/main/kotlin 90 | src/test/kotlin 91 | 92 | 93 | src/main/resources 94 | 95 | 96 | 97 | 98 | org.springframework.boot 99 | spring-boot-maven-plugin 100 | ${spring.boot.version} 101 | 102 | 103 | 104 | repackage 105 | 106 | 107 | 108 | 109 | 110 | org.jetbrains.kotlin 111 | kotlin-maven-plugin 112 | ${kotlin.version} 113 | 114 | 115 | compile 116 | compile 117 | 118 | compile 119 | 120 | 121 | 122 | test-compile 123 | test-compile 124 | 125 | test-compile 126 | 127 | 128 | 129 | 130 | 17 131 | 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /api/src/main/kotlin/io/codegeet/platform/api/Application.kt: -------------------------------------------------------------------------------- 1 | package io.codegeet.platform.api 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 | -------------------------------------------------------------------------------- /api/src/main/kotlin/io/codegeet/platform/api/auth/AuthConfiguration.kt: -------------------------------------------------------------------------------- 1 | package io.codegeet.platform.api.auth 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties 4 | import org.springframework.boot.context.properties.EnableConfigurationProperties 5 | import org.springframework.context.annotation.Configuration 6 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer 8 | 9 | @Configuration 10 | @EnableConfigurationProperties(AuthConfiguration.ApiKeyConfig::class) 11 | class AuthConfiguration(private val authInterceptor: AuthInterceptor) : WebMvcConfigurer { 12 | 13 | override fun addInterceptors(registry: InterceptorRegistry) { 14 | registry.addInterceptor(authInterceptor).addPathPatterns("/**") 15 | } 16 | 17 | @ConfigurationProperties(prefix = "app.api") 18 | data class ApiKeyConfig( 19 | val key: String 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /api/src/main/kotlin/io/codegeet/platform/api/auth/AuthInterceptor.kt: -------------------------------------------------------------------------------- 1 | package io.codegeet.platform.api.auth 2 | 3 | import io.codegeet.platform.api.auth.AuthConfiguration.ApiKeyConfig 4 | import jakarta.servlet.http.HttpServletRequest 5 | import jakarta.servlet.http.HttpServletResponse 6 | import org.springframework.http.HttpStatus 7 | import org.springframework.stereotype.Component 8 | import org.springframework.web.servlet.HandlerInterceptor 9 | 10 | @Component 11 | class AuthInterceptor(private val apiKeyConfig: ApiKeyConfig) : HandlerInterceptor { 12 | 13 | override fun preHandle( 14 | request: HttpServletRequest, 15 | response: HttpServletResponse, 16 | handler: Any 17 | ): Boolean { 18 | val apiKey = request.getHeader("X-API-KEY") 19 | 20 | if (apiKey != null && apiKey == apiKeyConfig.key) { 21 | return true 22 | } 23 | response.status = HttpStatus.FORBIDDEN.value() 24 | return false 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /api/src/main/kotlin/io/codegeet/platform/api/config/ApplicationConfiguration.kt: -------------------------------------------------------------------------------- 1 | package io.codegeet.platform.api.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 | @Configuration 14 | class ApplicationConfiguration { 15 | 16 | @Bean 17 | fun objectMapper(): ObjectMapper = Jackson2ObjectMapperBuilder.json() 18 | .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) 19 | .serializationInclusion(JsonInclude.Include.NON_NULL) 20 | .modulesToInstall(KotlinModule.Builder().build()) 21 | .build() 22 | 23 | @Bean 24 | fun messageConverter(objectMapper: ObjectMapper): Jackson2JsonMessageConverter = 25 | Jackson2JsonMessageConverter(ObjectMapper().registerModule(KotlinModule.Builder().build())) 26 | 27 | @Bean 28 | fun clock(): Clock = Clock.systemUTC() 29 | } 30 | -------------------------------------------------------------------------------- /api/src/main/kotlin/io/codegeet/platform/api/exceptions/CustomExceptionHandler.kt: -------------------------------------------------------------------------------- 1 | package io.codegeet.platform.api.exceptions 2 | 3 | import org.springframework.http.HttpStatus 4 | import org.springframework.web.bind.annotation.ResponseStatus 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | class ExecutionNotFoundException(message: String? = null) : RuntimeException(message) -------------------------------------------------------------------------------- /api/src/main/kotlin/io/codegeet/platform/api/executions/ExecutionReplyService.kt: -------------------------------------------------------------------------------- 1 | package io.codegeet.platform.api.executions 2 | 3 | import io.codegeet.platform.api.executions.model.ExecutionRepository 4 | import io.codegeet.platform.common.ExecutionResult 5 | import org.springframework.stereotype.Service 6 | import kotlin.jvm.optionals.getOrNull 7 | 8 | @Service 9 | class ExecutionReplyService( 10 | private val executionRepository: ExecutionRepository, 11 | ) { 12 | 13 | fun handle(executionId: String, result: ExecutionResult) { 14 | executionRepository.findById(executionId) 15 | .getOrNull() 16 | ?.let { execution -> 17 | executionRepository.save( 18 | execution.copy( 19 | status = result.status, 20 | error = result.error, 21 | invocations = execution.invocations.mapIndexed { i, invocation -> 22 | result.invocations.getOrNull(i)?.let { 23 | invocation.copy( 24 | status = it.status, 25 | stdOut = it.stdOut, 26 | stdErr = it.stdErr, 27 | runtime = it.details?.runtime, 28 | memory = it.details?.memory 29 | ) 30 | } ?: invocation.copy( 31 | status = null, 32 | stdErr = "Not found in output" 33 | ) 34 | }.toMutableList() 35 | ) 36 | ) 37 | } ?: let { 38 | // log error execution is not found 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /api/src/main/kotlin/io/codegeet/platform/api/executions/ExecutionService.kt: -------------------------------------------------------------------------------- 1 | package io.codegeet.platform.api.executions 2 | 3 | import io.codegeet.platform.api.exceptions.ExecutionNotFoundException 4 | import io.codegeet.platform.api.executions.model.Execution 5 | import io.codegeet.platform.api.executions.model.ExecutionRepository 6 | import io.codegeet.platform.api.executions.model.Invocation 7 | import io.codegeet.platform.api.job.JobClient 8 | import io.codegeet.platform.common.ExecutionRequest 9 | import org.springframework.stereotype.Service 10 | import java.time.Clock 11 | import java.time.Instant 12 | import java.time.temporal.ChronoUnit 13 | import java.util.* 14 | 15 | @Service 16 | class ExecutionService( 17 | private val executionRepository: ExecutionRepository, 18 | private val jobClient: JobClient, 19 | private val clock: Clock, 20 | ) { 21 | 22 | fun execute(request: ExecutionRequest, sync: Boolean = false): Execution { 23 | val execution = executionRepository.save(toExecution(request)) 24 | 25 | jobClient.submit(execution.executionId, request) 26 | return execution 27 | } 28 | 29 | fun get(executionId: String): Execution { 30 | return executionRepository.findById(executionId) 31 | .orElseThrow { ExecutionNotFoundException("Execution with id: $executionId not found") } 32 | } 33 | 34 | private fun toExecution(request: ExecutionRequest) = request.toExecution( 35 | executionId = UUID.randomUUID().toString(), 36 | now = Instant.now(clock).truncatedTo(ChronoUnit.MILLIS) 37 | ) 38 | 39 | private fun ExecutionRequest.toExecution(executionId: String, now: Instant) = Execution( 40 | executionId = executionId, 41 | code = this.code, 42 | language = this.language, 43 | status = null, 44 | createdAt = now, 45 | ).also { execution -> 46 | execution.invocations.addAll(this.invocations.takeIf { it.isNotEmpty() } 47 | ?.map { 48 | Invocation( 49 | executionId = execution.executionId, 50 | status = null, 51 | arguments = it.args?.joinToString("\n"), 52 | stdIn = it.stdIn, 53 | ) 54 | } ?: listOf( 55 | Invocation( 56 | executionId = execution.executionId, 57 | status = null, 58 | arguments = null, 59 | stdIn = null, 60 | ) 61 | )) 62 | } 63 | 64 | private fun Execution.toJob() = 65 | ExecutionRequest( 66 | code = this.code, 67 | language = this.language, 68 | invocations = this.invocations.map { 69 | ExecutionRequest.InvocationRequest( 70 | args = it.arguments?.split("\n"), 71 | stdIn = it.stdIn 72 | ) 73 | }, 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /api/src/main/kotlin/io/codegeet/platform/api/executions/model/ExecutionRepository.kt: -------------------------------------------------------------------------------- 1 | package io.codegeet.platform.api.executions.model 2 | 3 | import org.springframework.data.mongodb.repository.MongoRepository 4 | 5 | interface ExecutionRepository : MongoRepository 6 | -------------------------------------------------------------------------------- /api/src/main/kotlin/io/codegeet/platform/api/executions/model/Model.kt: -------------------------------------------------------------------------------- 1 | package io.codegeet.platform.api.executions.model 2 | 3 | import io.codegeet.platform.common.ExecutionStatus 4 | import io.codegeet.platform.common.InvocationStatus 5 | import io.codegeet.platform.common.language.Language 6 | import org.springframework.data.annotation.Id 7 | import org.springframework.data.mongodb.core.mapping.Document 8 | import java.time.Instant 9 | 10 | @Document(collection = "executions") 11 | data class Execution( 12 | @Id 13 | val executionId: String, 14 | val language: Language, 15 | val code: String, 16 | val status: ExecutionStatus?, 17 | var error: String? = null, 18 | 19 | val invocations: MutableList = mutableListOf(), 20 | 21 | val createdAt: Instant, 22 | val totalTime: Int? = null, 23 | ) 24 | 25 | data class Invocation( 26 | val executionId: String, 27 | var status: InvocationStatus?, 28 | val arguments: String?, 29 | val stdIn: String?, 30 | val stdOut: String? = null, 31 | val stdErr: String? = null, 32 | val runtime: Long? = null, 33 | val memory: Long? = null, 34 | ) 35 | -------------------------------------------------------------------------------- /api/src/main/kotlin/io/codegeet/platform/api/job/JobClient.kt: -------------------------------------------------------------------------------- 1 | package io.codegeet.platform.api.job 2 | 3 | import io.codegeet.platform.api.executions.ExecutionReplyService 4 | import io.codegeet.platform.common.ExecutionJobReply 5 | import io.codegeet.platform.common.ExecutionJobRequest 6 | import io.codegeet.platform.common.ExecutionRequest 7 | import mu.KLogging 8 | import org.springframework.amqp.rabbit.core.RabbitTemplate 9 | import org.springframework.stereotype.Service 10 | 11 | @Service 12 | class JobClient( 13 | private val requestTemplate: RabbitTemplate, 14 | private val requestQueueConfig: JobConfiguration.RequestQueueConfig, 15 | private val replyHandler: ExecutionReplyService, 16 | ) { 17 | 18 | companion object : KLogging() { 19 | const val RECEIVE_METHOD_NAME = "receive" 20 | } 21 | 22 | fun submit(executionId: String, request: ExecutionRequest) { 23 | logger.info("Submit execution request for executionId: $executionId") 24 | requestTemplate.convertAndSend( 25 | requestQueueConfig.exchange, 26 | requestQueueConfig.routingKey, 27 | ExecutionJobRequest(executionId, request) 28 | ) 29 | } 30 | 31 | fun receive(response: ExecutionJobReply) { 32 | logger.info("Received execution result for executionId: ${response.executionId}") 33 | replyHandler.handle(response.executionId, response.result) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /api/src/main/kotlin/io/codegeet/platform/api/job/JobConfiguration.kt: -------------------------------------------------------------------------------- 1 | package io.codegeet.platform.api.job 2 | 3 | import org.springframework.amqp.core.Binding 4 | import org.springframework.amqp.core.BindingBuilder 5 | import org.springframework.amqp.core.DirectExchange 6 | import org.springframework.amqp.core.Queue 7 | import org.springframework.amqp.rabbit.connection.ConnectionFactory 8 | import org.springframework.amqp.rabbit.core.RabbitTemplate 9 | import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer 10 | import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter 11 | import org.springframework.amqp.support.converter.MessageConverter 12 | import org.springframework.boot.context.properties.ConfigurationProperties 13 | import org.springframework.boot.context.properties.EnableConfigurationProperties 14 | import org.springframework.context.annotation.Bean 15 | import org.springframework.context.annotation.Configuration 16 | 17 | @Configuration 18 | @EnableConfigurationProperties( 19 | value = [ 20 | JobConfiguration.RequestQueueConfig::class, 21 | JobConfiguration.ReplyQueueConfig::class 22 | ] 23 | ) 24 | class JobConfiguration { 25 | 26 | // submit requests to execution job 27 | 28 | @Bean 29 | fun producerQueue(config: RequestQueueConfig): Queue { 30 | return Queue(config.name, false) 31 | } 32 | 33 | @Bean 34 | fun producerExchange(config: RequestQueueConfig): DirectExchange { 35 | return DirectExchange(config.exchange) 36 | } 37 | 38 | @Bean 39 | fun producerBinding(producerQueue: Queue, producerExchange: DirectExchange, config: RequestQueueConfig): Binding = 40 | BindingBuilder.bind(producerQueue).to(producerExchange).with(config.routingKey) 41 | 42 | @Bean 43 | fun producerTemplate(connectionFactory: ConnectionFactory, jsonMessageConverter: MessageConverter): RabbitTemplate = 44 | RabbitTemplate(connectionFactory).also { it.messageConverter = jsonMessageConverter } 45 | 46 | // process execution job responses 47 | 48 | @Bean 49 | fun consumerQueue(config: ReplyQueueConfig): Queue = Queue(config.name, false) 50 | 51 | @Bean 52 | fun simpleMessageListenerContainer( 53 | connectionFactory: ConnectionFactory, 54 | messageListenerAdapter: MessageListenerAdapter, 55 | config: ReplyQueueConfig 56 | ) = SimpleMessageListenerContainer().also { 57 | it.setConnectionFactory(connectionFactory) 58 | it.setQueueNames(config.name) 59 | it.setMessageListener(messageListenerAdapter) 60 | } 61 | 62 | @Bean 63 | fun messageListenerAdapter( 64 | receiver: JobClient, 65 | messageConverter: MessageConverter 66 | ): MessageListenerAdapter = MessageListenerAdapter(receiver, JobClient.RECEIVE_METHOD_NAME).also { 67 | it.setMessageConverter(messageConverter) 68 | } 69 | 70 | @ConfigurationProperties(prefix = "app.job.queue.reply") 71 | data class ReplyQueueConfig( 72 | val name: String 73 | ) 74 | 75 | @ConfigurationProperties(prefix = "app.job.queue.request") 76 | data class RequestQueueConfig( 77 | val name: String, 78 | val exchange: String, 79 | val routingKey: String, 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /api/src/main/kotlin/io/codegeet/platform/api/resource/ExecutionResource.kt: -------------------------------------------------------------------------------- 1 | package io.codegeet.platform.api.resource 2 | 3 | import io.codegeet.platform.api.executions.ExecutionService 4 | import io.codegeet.platform.api.executions.model.Execution 5 | import io.codegeet.platform.common.ExecutionStatus 6 | import io.codegeet.platform.common.InvocationStatus 7 | import io.codegeet.platform.common.language.Language 8 | import org.springframework.stereotype.Controller 9 | import org.springframework.web.bind.annotation.* 10 | 11 | @Controller 12 | @RequestMapping("api/executions") 13 | class ExecutionResource(private val executionService: ExecutionService) { 14 | 15 | @PostMapping 16 | @ResponseBody 17 | fun post(@RequestBody body: ExecutionRequest): ExecutionResponse { 18 | return executionService.execute(body.toRequest()).toResponse() 19 | } 20 | 21 | @GetMapping("{executionId}") 22 | @ResponseBody 23 | fun get(@PathVariable executionId: String): ExecutionResponse { 24 | return executionService.get(executionId).toResponse() 25 | } 26 | 27 | data class ExecutionRequest( 28 | val code: String, 29 | val language: Language, 30 | val invocations: List = emptyList(), 31 | ) { 32 | data class InvocationInput( 33 | val stdIn: String? = null, 34 | val args: List? = null, 35 | ) 36 | } 37 | 38 | data class ExecutionResponse( 39 | val executionId: String, 40 | val status: ExecutionStatus?, 41 | val time: Int?, 42 | val error: String?, 43 | val invocations: List? = null 44 | ) { 45 | data class InvocationOutput( 46 | val status: InvocationStatus?, 47 | val details: InvocationDetails?, 48 | val stdOut: String?, 49 | val stdErr: String? 50 | ) 51 | 52 | data class InvocationDetails( 53 | val runtime: Long?, 54 | val memory: Long?, 55 | ) 56 | } 57 | 58 | private fun ExecutionRequest.toRequest() = io.codegeet.platform.common.ExecutionRequest( 59 | code = this.code, 60 | language = this.language, 61 | invocations = this.invocations.map { 62 | io.codegeet.platform.common.ExecutionRequest.InvocationRequest( 63 | args = it.args, 64 | stdIn = it.stdIn 65 | ) 66 | } 67 | ) 68 | 69 | private fun Execution.toResponse() = ExecutionResponse( 70 | executionId = this.executionId, 71 | status = this.status, 72 | error = this.error, 73 | time = this.totalTime, 74 | invocations = this.invocations 75 | .filter { status != null } 76 | .map { 77 | ExecutionResponse.InvocationOutput( 78 | status = it.status, 79 | stdOut = it.stdOut, 80 | stdErr = it.stdErr, 81 | details = ExecutionResponse.InvocationDetails(runtime = it.runtime, memory = it.memory) 82 | ) 83 | } 84 | .ifEmpty { null } 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /api/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8080 2 | 3 | server.error.include-stacktrace=never 4 | 5 | #docker run --name codegeet-postgres -e POSTGRES_PASSWORD=codegeet -d -p 5432:5432 postgres 6 | 7 | #spring.datasource.url=jdbc:postgresql://localhost/postgres 8 | #spring.datasource.username=postgres 9 | #spring.datasource.password=codegeet 10 | #spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect 11 | #spring.jpa.hibernate.ddl-auto=update 12 | 13 | app.job.queue.request.name=execution.request.queue 14 | app.job.queue.request.exchange=execution.exchange 15 | app.job.queue.request.routingKey=execution.request.routingKey 16 | 17 | app.job.queue.reply.name=execution.reply.queue -------------------------------------------------------------------------------- /api/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 | -------------------------------------------------------------------------------- /coderunner/README.md: -------------------------------------------------------------------------------- 1 | # Code Runner 2 | 3 | ## Overview 4 | 5 | **Coderunner** is a command-line tool that runs inside Codegeet Docker containers 6 | and used for compiling and running code. 7 | When the container starts, **coderunner** reads input from `stdin` in JSON format, 8 | compiles and executes the code, and outputs the results to `stdout`, also in JSON format. 9 | 10 | ### Json Input 11 | ```json 12 | { 13 | "code": "print(f\"Hello, CodeGeet!\")", 14 | "language": "python" 15 | } 16 | ``` 17 | 18 | ### Json Output 19 | ```json 20 | { 21 | "status": "SUCCESS", 22 | "invocations": [ 23 | { 24 | "status": "SUCCESS", 25 | "std_out": "Hello, CodeGeet!\n" 26 | } 27 | ] 28 | } 29 | ``` 30 | 31 | ## Run locally (Linux and MacOS) 32 | To execute code snippets using **coderunner** on your local machine, ensure you have installed the necessary dependencies for the language you want to run (e.g., install a Python interpreter for Python). 33 | Once dependencies are installed, you can try executing some code. For example, to run a Python snippet, use the following command: 34 | ```bash 35 | echo '{ 36 | "code": "print(f\"Hello, CodeGeet!\")", 37 | "language": "python" 38 | }' | java -jar coderunner.jar 39 | ``` 40 | #### Result: 41 | ```json 42 | { 43 | "status" : "SUCCESS", 44 | "invocations" : [ 45 | { 46 | "status" : "SUCCESS", 47 | "std_out" : "Hello, CodeGeet!\n", 48 | "std_err" : "" 49 | }] 50 | } 51 | ``` 52 | Your code can read from `std_in` and use command line `args`: 53 | 54 | #### std_in: 55 | ```bash 56 | echo '{ 57 | "code": "print(f\"Hello, {input()}!\")", 58 | "language": "python", 59 | "invocations": [ 60 | { 61 | "std_in": "CodeGeet" 62 | } 63 | ] 64 | }' | ./coderunner.sh 65 | ``` 66 | #### args: 67 | ```bash 68 | echo '{ 69 | "code": "import sys; print(f\"Hello, {sys.argv[1]}!\")", 70 | "language": "python", 71 | "invocations": [ 72 | { 73 | "args": ["CodeGeet"] 74 | } 75 | ] 76 | }' | ./coderunner.sh 77 | ```` 78 | 79 | ### Compilation and multiple executions 80 | When code compilation is needed, **coderunner** will compile the code with the provided 'compile' instruction 81 | (e.g., `compile: javac Main.java`) and execute it with 'exec' (e.g., `exec: java Main`). 82 | Additionally, you can have multiple executions of compiled code: 83 | #### Example: 84 | ```bash 85 | echo '{ 86 | "code": "class Main { public static void main(String[] args) { System.out.print(args[0]); }}", 87 | "language": "java", 88 | "invocations": [ 89 | { 90 | "args": ["one"] 91 | }, 92 | { 93 | "args": ["another"] 94 | } 95 | ] 96 | }' | ./coderunner.sh 97 | ```` 98 | #### Output 99 | ```json: 100 | { 101 | "status" : "SUCCESS", 102 | "invocations" : [ { 103 | "status" : "SUCCESS", 104 | "std_out" : "one", 105 | "std_err" : "" 106 | }, { 107 | "status" : "SUCCESS", 108 | "std_out" : "another", 109 | "std_err" : "" 110 | } ] 111 | } 112 | ```` 113 | ### Statistics 114 | If you have `/usr/bin/time` installed on Linux or `gtime` on Mac, **coderunner** will return memory statistics for the execution: 115 | ```json: 116 | { 117 | "status" : "SUCCESS", 118 | "compilation" : { 119 | "details" : { 120 | "runtime" : 251, 121 | "memory" : 78672 122 | } 123 | }, 124 | "invocations" : [ { 125 | "status" : "SUCCESS", 126 | "details" : { 127 | "runtime" : 48, 128 | "memory" : 34880 129 | }, 130 | "std_out" : "one", 131 | "std_err" : "" 132 | }, { 133 | "status" : "SUCCESS", 134 | "details" : { 135 | "runtime" : 48, 136 | "memory" : 35040 137 | }, 138 | "std_out" : "another", 139 | "std_err" : "" 140 | } ] 141 | } 142 | ```` 143 | 144 | You can install `/usr/bin/time` or `gtime` by running: 145 | 146 | ```bash 147 | brew install gnu-time 148 | ```` 149 | ```bash 150 | sudo apt install time 151 | ```` 152 | -------------------------------------------------------------------------------- /coderunner/coderunner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | java -jar coderunner.jar "$@" <&0 -------------------------------------------------------------------------------- /coderunner/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | io.codegeet 8 | platform 9 | 0.1.0-SNAPSHOT 10 | 11 | 12 | coderunner 13 | jar 14 | 15 | 16 | 17 | io.codegeet 18 | common 19 | ${project.version} 20 | 21 | 22 | org.jetbrains.kotlin 23 | kotlin-stdlib 24 | ${kotlin.version} 25 | 26 | 27 | com.fasterxml.jackson.module 28 | jackson-module-kotlin 29 | 30 | 31 | org.junit.jupiter 32 | junit-jupiter-engine 33 | 5.9.3 34 | test 35 | 36 | 37 | org.mockito.kotlin 38 | mockito-kotlin 39 | 5.2.1 40 | test 41 | 42 | 43 | 44 | 45 | src/main/kotlin 46 | src/test/kotlin 47 | 48 | 49 | org.jetbrains.kotlin 50 | kotlin-maven-plugin 51 | ${kotlin.version} 52 | 53 | 17 54 | 55 | 56 | 57 | compile 58 | 59 | compile 60 | 61 | 62 | 63 | test-compile 64 | 65 | test-compile 66 | 67 | 68 | 69 | 70 | 71 | org.apache.maven.plugins 72 | maven-assembly-plugin 73 | 3.6.0 74 | 75 | 76 | make-assembly 77 | package 78 | 79 | single 80 | 81 | 82 | 83 | 84 | io.codegeet.platform.coderunner.ApplicationKt 85 | 86 | 87 | 88 | jar-with-dependencies 89 | 90 | 91 | 92 | 93 | 94 | 95 | org.apache.maven.plugins 96 | maven-compiler-plugin 97 | 98 | 17 99 | 17 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /coderunner/src/main/kotlin/io/codegeet/platform/coderunner/Application.kt: -------------------------------------------------------------------------------- 1 | package io.codegeet.platform.coderunner 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude 4 | import com.fasterxml.jackson.databind.JsonMappingException 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | import com.fasterxml.jackson.databind.PropertyNamingStrategies 7 | import com.fasterxml.jackson.databind.SerializationFeature 8 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 9 | import io.codegeet.platform.common.ExecutionRequest 10 | import io.codegeet.platform.common.ExecutionResult 11 | import io.codegeet.platform.common.ExecutionStatus 12 | 13 | private val objectMapper: ObjectMapper = jacksonObjectMapper().apply { 14 | propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE 15 | setSerializationInclusion(JsonInclude.Include.NON_NULL) 16 | enable(SerializationFeature.INDENT_OUTPUT) 17 | } 18 | 19 | fun main(args: Array) { 20 | val runner = Runner(ProcessExecutor(stats = ProcessStats)) 21 | 22 | val result = try { 23 | parseInput()?.let { runner.run(it) } 24 | ?: ExecutionResult( 25 | status = ExecutionStatus.INTERNAL_ERROR, 26 | error = "Coderunner input is empty." 27 | ) 28 | } catch (e: JsonMappingException) { 29 | ExecutionResult( 30 | status = ExecutionStatus.INTERNAL_ERROR, 31 | error = "Failed to parse coderunner input: ${e.message}" 32 | ) 33 | } 34 | 35 | println(result.toJson()) 36 | } 37 | 38 | private fun parseInput(): ExecutionRequest? { 39 | val stringBuilder = StringBuilder() 40 | while (true) { 41 | val line = readlnOrNull() 42 | if (line.isNullOrEmpty()) 43 | break 44 | stringBuilder.append(line).append("\n") 45 | } 46 | 47 | return stringBuilder.toString() 48 | .takeIf { it.isNotEmpty() } 49 | ?.let { objectMapper.readValue(it, ExecutionRequest::class.java) } 50 | } 51 | 52 | private fun ExecutionResult.toJson(): String = 53 | objectMapper.writeValueAsString(this) -------------------------------------------------------------------------------- /coderunner/src/main/kotlin/io/codegeet/platform/coderunner/ProcessExecutor.kt: -------------------------------------------------------------------------------- 1 | package io.codegeet.platform.coderunner 2 | 3 | import io.codegeet.platform.coderunner.exception.OutputLimitException 4 | import io.codegeet.platform.coderunner.exception.TimeLimitException 5 | import java.io.File 6 | import java.io.InputStream 7 | import java.util.concurrent.TimeUnit 8 | 9 | class ProcessExecutor(private val stats: ProcessStats) { 10 | 11 | companion object { 12 | private const val DEFAULT_TIMEOUT_MILLIS: Long = 10_000 13 | private const val DEFAULT_OUTPUT_LIMIT_BYTES: Int = 5_000 14 | } 15 | 16 | fun execute(command: List): ProcessData { 17 | return execute(command, null, DEFAULT_TIMEOUT_MILLIS) 18 | } 19 | 20 | fun execute(command: List, input: String?, timeout: Long): ProcessData { 21 | val processBuilder = ProcessBuilder(stats.wrapCommand(command)) 22 | .directory(File(getUserHomeDirectory())) 23 | 24 | val (process, time) = stats.withTime { 25 | processBuilder.start() 26 | .also { 27 | it.outputStream.use { it.write(input.orEmpty().toByteArray()) } 28 | it.waitFor(timeout, TimeUnit.MILLISECONDS) 29 | } 30 | } 31 | 32 | if (process.isAlive) { 33 | process.destroy() 34 | throw TimeLimitException(timeout, TimeUnit.MILLISECONDS) 35 | } 36 | 37 | val (errorStream, memory) = process.errorStream.readAsString() 38 | .let { stats.withMemory(it) } 39 | 40 | val outputStream = process.inputStream.readAsString() 41 | 42 | return ProcessData( 43 | time = time, 44 | memory = memory, 45 | stdOut = outputStream, 46 | stdErr = errorStream, 47 | completed = process.exitValue() == 0 48 | ) 49 | } 50 | 51 | private fun getUserHomeDirectory(): String { 52 | return System.getProperty("user.home") 53 | } 54 | 55 | private fun InputStream.readAsString(limit: Int = DEFAULT_OUTPUT_LIMIT_BYTES): String = this.use { inputStream -> 56 | String(inputStream.readNBytes(limit)) 57 | .also { 58 | if (inputStream.available() > 0) throw OutputLimitException(limit) 59 | } 60 | } 61 | 62 | data class ProcessData( 63 | val time: Long, 64 | val memory: Long?, 65 | val stdOut: String, 66 | val stdErr: String, 67 | val completed: Boolean, 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /coderunner/src/main/kotlin/io/codegeet/platform/coderunner/ProcessStats.kt: -------------------------------------------------------------------------------- 1 | package io.codegeet.platform.coderunner 2 | 3 | import java.lang.Exception 4 | 5 | object ProcessStats { 6 | 7 | private const val REGEX = "\\[(\\d+)\\]\\n?$" 8 | 9 | private var statsCommand: List = emptyList() 10 | 11 | init { 12 | statsCommand = buildStatsCall() 13 | } 14 | 15 | fun wrapCommand(command: List) = 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 --------------------------------------------------------------------------------