commit c0e8690bae308c4d75f8a5901ff7ff9a88acadaa Author: vitaliy Date: Fri Jun 20 15:11:48 2025 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..d438b1e --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Code Runner + +Миссия проекта **Code Runner** — предоставить платформу, где студенты могут совершенствовать свои навыки программирования посредством решения различных задач на разных языках программирования. + + +## Поддержа языков + +Платформа поддерживает 1 язык программирования Python. + +## Дизайн системы + +![Описание изображения](doc/services.png) + + +## Рабочие Endpoints: + +### Frontend Service + +- `GET /task/{id}` \- Получить задачу по UUID + +### REST API Service + +- `POST /run` \- Отправить код на проверку diff --git a/cmd/agent_service.go b/cmd/agent_service.go new file mode 100644 index 0000000..48faf32 --- /dev/null +++ b/cmd/agent_service.go @@ -0,0 +1,167 @@ +package main + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "log" + "os" + "os/exec" + + "github.com/streadway/amqp" +) + +func main() { + + type Code struct { + IDTask string `json:"id_task"` + TpRunner string `json:"tp_runner"` + Code string `json:"code"` + Test string `json:"test"` + } + + // Define RabbitMQ server URL. + amqpServerURL := os.Getenv("AMQP_SERVER_URL") + if amqpServerURL == "" { + amqpServerURL = "amqp://guest:guest@localhost:5672/" // Default URL if not set + } + + // Create a new RabbitMQ connection. + connectRabbitMQ, err := amqp.Dial(amqpServerURL) + if err != nil { + panic(err) + } + defer connectRabbitMQ.Close() + + // Opening a channel to our RabbitMQ instance over + // the connection we have already established. + channelRabbitMQ, err := connectRabbitMQ.Channel() + if err != nil { + panic(err) + } + defer channelRabbitMQ.Close() + + // Subscribing to QueueService1 for getting messages. + messages, err := channelRabbitMQ.Consume( + "QueueService1", // queue name + "", // consumer + true, // auto-ack + false, // exclusive + false, // no local + false, // no wait + nil, // arguments + ) + if err != nil { + log.Println(err) + } + + // Build a welcome message. + log.Println("Successfully connected to RabbitMQ") + log.Println("Waiting for messages") + + // Make a channel to receive messages into infinite loop. + forever := make(chan bool) + + go func() { + for message := range messages { + // For example, show received message in a console. + log.Printf(" > Received message: %s\n", message.Body) + // Создание экземпляра структуры Task + + var code_obj Code + + // Разбор JSON строки в структуру + err := json.Unmarshal([]byte(message.Body), &code_obj) + if err != nil { + log.Fatalf("Ошибка при разборе JSON: %v", err) + } + mkdir(code_obj.IDTask) + mkfile(code_obj.IDTask, code_obj.Code, "main.py") + mkfile(code_obj.IDTask, code_obj.Test, "test_hello.py") + run(code_obj.IDTask) + + fmt.Println(code_obj.Code) + + } + }() + + <-forever +} + +func mkdir(dirName string) { + // Имя директории, которую нужно создать + + // Права доступа к директории (0755 - чтение, запись, выполнение для владельца, чтение и выполнение для группы и остальных) + permissions := os.ModeDir | 0755 + + // Создание директории + err := os.Mkdir(dirName, permissions) + if err != nil { + log.Printf("Ошибка при создании директории: %v", err) + } + + fmt.Printf("Директория '%s' успешно создана.\n", dirName) +} + +func mkfile(dirName string, base64String string, fName string) { + + // Декодирование Base64 строки + decodedBytes, er := base64.StdEncoding.DecodeString(base64String) + if er != nil { + log.Fatalf("Ошибка при декодировании Base64: %v", er) + } + + // Преобразование байтов в строку + fileContent := string(decodedBytes) + + // Права доступа к файлу (0644 - чтение и запись для владельца, чтение для группы и остальных) + fileName := "./" + dirName + "/" + fName + permissions := 0644 + + // Преобразование содержимого в слайс байтов + data := []byte(fileContent) + + // Создание файла и запись содержимого + err := os.WriteFile(fileName, data, os.FileMode(permissions)) + if err != nil { + log.Fatalf("Ошибка при создании файла: %v", err) + } + + fmt.Printf("Файл '%s' успешно создан с содержимым.\n", fileName) + +} + +func run(idTask string) { + + // Получение текущей рабочей директории + currentDir, err := os.Getwd() + if err != nil { + log.Fatalf("Ошибка при получении текущей директории: %v", err) + } + + dir := currentDir + "/" + idTask + + // Изменение текущей рабочей директории + err = os.Chdir(dir) + if err != nil { + log.Fatalf("Ошибка при изменении директории: %v", err) + } + + // Команда для запуска Python с кодом + fileName := "test_hello.py" + + + cmd := exec.Command("python", "-m", "unittest", fileName) + + // Запуск команды и получение вывода + output, err := cmd.CombinedOutput() + if err != nil { + log.Fatalf("Ошибка при выполнении Python кода: %v\nВывод: %s", err, string(output)) + } + + // Вывод результата + // todo: записать в топик RMQ id_user_id_UUID + // { data: output} + fmt.Printf("Вывод Python:\n%s\n", string(output)) + +} diff --git a/cmd/frontend_service.go b/cmd/frontend_service.go new file mode 100644 index 0000000..d67be89 --- /dev/null +++ b/cmd/frontend_service.go @@ -0,0 +1,115 @@ +package main + +import ( + "context" + "database/sql" + "fmt" + "log" + "net/http" + + "github.com/gin-gonic/gin" + _ "github.com/lib/pq" // Import the PostgreSQL driver + "github.com/redis/go-redis/v9" +) + +const ( + dbHost = "localhost" + dbPort = 5432 + dbUser = "postgres" + dbPassword = "123" // Замените на свой пароль + dbName = "auth" +) + +func main() { + + // Создание клиента Redis + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", // Адрес Redis сервера + Password: "", // Пароль, если требуется + DB: 0, // Номер базы данных + }) + + // Проверка подключения + ctx := context.Background() + pong, err := rdb.Ping(ctx).Result() + if err != nil { + fmt.Println("Ошибка подключения к Redis:", err) + return + } + + fmt.Println("Успешное подключение к Redis! Ответ:", pong) + + // Строка подключения к базе данных + connStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", + dbHost, dbPort, dbUser, dbPassword, dbName) + + // to-do: написать обращение в кеш Redis + // Подключение к базе данных + db, err := sql.Open("postgres", connStr) + if err != nil { + log.Fatal(err) + } + defer db.Close() + + // Проверка подключения + err = db.Ping() + if err != nil { + log.Fatal(err) + } + + fmt.Println("Connected to the database!") + + router := gin.Default() + router.LoadHTMLGlob("./templates/*") + + // Обработчик для получения фрагмента кода + router.GET("/task/:id", func(c *gin.Context) { + var code string + + id := c.Param("id") + code, err := rdb.Get(ctx, id).Result() + if err != nil { + err := db.QueryRow("SELECT json_text FROM task WHERE id = $1", id).Scan(&code) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + // Пример использования: установка и получение значения + err = rdb.Set(ctx, id, code, 0).Err() + if err != nil { + fmt.Println("Ошибка при установке значения:", err) + return + } + fmt.Println("Установили значение по ключу:", err) + } + c.JSON(http.StatusOK, gin.H{"code": code}) + }) + + // Обработчик для обновления фрагмента кода + router.POST("/task/:id", func(c *gin.Context) { + id := c.Param("id") + var requestBody struct { + Code string `json:"code" binding:"required"` + } + + if err := c.BindJSON(&requestBody); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + _, err := db.Exec("UPDATE snippets SET code = $1 WHERE id = $2", requestBody.Code, id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Snippet updated successfully"}) + }) + + router.GET("/", func(c *gin.Context) { + c.HTML(http.StatusOK, "index.html", gin.H{}) + }) + + // Запуск сервера + router.Run(":8080") +} diff --git a/cmd/push_service.go b/cmd/push_service.go new file mode 100644 index 0000000..27b2416 --- /dev/null +++ b/cmd/push_service.go @@ -0,0 +1,138 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + + "os" + "github.com/streadway/amqp" + "github.com/gorilla/websocket" +) + + + +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true // Allow all origins (for development). In production, specify allowed origins. + }, +} + +func handleConnections(w http.ResponseWriter, r *http.Request) { + + type Code struct { + IDTask string `json:"id_task"` + TpRunner string `json:"tp_runner"` + Code string `json:"code"` + Test string `json:"test"` + } + + // Define RabbitMQ server URL. + amqpServerURL := os.Getenv("AMQP_SERVER_URL") + if amqpServerURL == "" { + amqpServerURL = "amqp://guest:guest@localhost:5672/" // Default URL if not set + } + + // Create a new RabbitMQ connection. + connectRabbitMQ, err := amqp.Dial(amqpServerURL) + if err != nil { + panic(err) + } + defer connectRabbitMQ.Close() + + // Opening a channel to our RabbitMQ instance over + // the connection we have already established. + channelRabbitMQ, err := connectRabbitMQ.Channel() + if err != nil { + panic(err) + } + defer channelRabbitMQ.Close() + + + // todo: заменен QueueService1 на динамический + // id_user_id_task + // Subscribing to QueueService1 for getting messages. + messages, err := channelRabbitMQ.Consume( + "QueueService1", // queue name + "", // consumer + true, // auto-ack + false, // exclusive + false, // no local + false, // no wait + nil, // arguments + ) + if err != nil { + log.Println(err) + } + + // Build a welcome message. + log.Println("Successfully connected to RabbitMQ") + log.Println("Waiting for messages") + + // Upgrade initial GET request to a websocket + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Fatal(err) + } + // Make sure we close the connection when the function returns + defer ws.Close() + + + // Make a channel to receive messages into infinite loop. + forever := make(chan bool) + + go func() { + for message := range messages { + // For example, show received message in a console. + log.Printf(" > Received message: %s\n", message.Body) + // Создание экземпляра структуры Task + + var code_obj Code + + // Разбор JSON строки в структуру + err := json.Unmarshal([]byte(message.Body), &code_obj) + if err != nil { + log.Fatalf("Ошибка при разборе JSON: %v", err) + } + // Write message back to browser + err = ws.WriteMessage(websocket.TextMessage, []byte(code_obj.Code)) + if err != nil { + log.Println(err) + return + } + fmt.Println(code_obj.Code) + + } + }() + + <-forever + + + // for { + // // Read message from browser + // _, msg, err := ws.ReadMessage() + // if err != nil { + // log.Println(err) + // return + // } + + // // Print the message to the console + // fmt.Printf("Received: %s\n", msg) + + // // Write message back to browser + // err = ws.WriteMessage(websocket.TextMessage, msg) + // if err != nil { + // log.Println(err) + // return + // } + // } +} + +func main() { + + fmt.Println("Starting WebSocket server...") + http.HandleFunc("/ws", handleConnections) + + log.Fatal(http.ListenAndServe(":8081", nil)) +} \ No newline at end of file diff --git a/cmd/rest_service.go b/cmd/rest_service.go new file mode 100644 index 0000000..9fbf493 --- /dev/null +++ b/cmd/rest_service.go @@ -0,0 +1,123 @@ +package main + +import ( + "fmt" + "log" + "os" + + "encoding/json" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/gofiber/fiber/v2/middleware/logger" + "github.com/streadway/amqp" +) + +// Структура для JSON данных +type Code struct { + Id_task string `json:"id_task"` + Tp_runner string `json:"tp_runner"` + Code string `json:"code"` + Test string `json:"test"` +} + +func main() { + // Define RabbitMQ server URL. + amqpServerURL := os.Getenv("AMQP_SERVER_URL") + if amqpServerURL == "" { + amqpServerURL = "amqp://guest:guest@localhost:5672/" // Default URL if not set + } + + // Create a new RabbitMQ connection. + connectRabbitMQ, err := amqp.Dial(amqpServerURL) + if err != nil { + panic(err) + } + defer connectRabbitMQ.Close() + + // Let's start by opening a channel to our RabbitMQ + // instance over the connection we have already + // established. + channelRabbitMQ, err := connectRabbitMQ.Channel() + if err != nil { + panic(err) + } + defer channelRabbitMQ.Close() + + // With the instance and declare Queues that we can + // publish and subscribe to. + _, err = channelRabbitMQ.QueueDeclare( + "QueueService1", // queue name + true, // durable + false, // auto delete + false, // exclusive + false, // no wait + nil, // arguments + ) + if err != nil { + panic(err) + } + + // Create a new Fiber instance. + app := fiber.New() + + app.Use(cors.New(cors.Config{ + AllowOrigins: "*", + AllowMethods: "GET,POST,HEAD,PUT,DELETE,PATCH", + AllowHeaders: "Origin, Content-Type, Accept, Authorization", + })) + + // Add middleware. + app.Use( + logger.New(), // add simple logger + ) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, CORS is enabled!") + }) + // Add route. ?msg=bla + + app.Post("/run", func(c *fiber.Ctx) error { + //todo: Тестирование ручки на взаимодействие + // c клиентом. Расширить json добавив ID_USER + // из AUTH SERVICE + + // Create a message to publish. + // to-do: дописать приемку json + mess := new(Code) + + // Разбираем JSON из тела запроса и заполняем структуру + _ = c.BodyParser(mess) + jsonBody, err := json.Marshal(mess) + if err != nil { + log.Fatalf("Error encoding JSON: %v", err) + } + + fmt.Println(mess.Code) + message := amqp.Publishing{ + ContentType: "application/json", + Body: jsonBody, + } + + // message := amqp.Publishing{ + // ContentType: "text/plain", + // Body: []byte("Hello"), + // } + + // Attempt to publish a message to the queue. + if err := channelRabbitMQ.Publish( + "", // exchange + "QueueService1", // queue name + false, // mandatory + false, // immediate + message, // message to publish + ); err != nil { + return err + } + + return nil + }) + + // Start Fiber API server. + log.Fatal(app.Listen(":3000")) +} diff --git a/cmd/templates/chat.html b/cmd/templates/chat.html new file mode 100644 index 0000000..e6edc58 --- /dev/null +++ b/cmd/templates/chat.html @@ -0,0 +1,37 @@ + + + + WebSocket Client + + +

WebSocket Client

+ + +
+ + + + \ No newline at end of file diff --git a/cmd/templates/index.html b/cmd/templates/index.html new file mode 100644 index 0000000..484ece0 --- /dev/null +++ b/cmd/templates/index.html @@ -0,0 +1,97 @@ + + + + + + Code Editor + + + + + + +

Code Editor

+ + + + +
+ + + + \ No newline at end of file diff --git a/doc/services.png b/doc/services.png new file mode 100644 index 0000000..086833f Binary files /dev/null and b/doc/services.png differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a066529 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.gocommunity.ru/chertkov/code_runner + +go 1.23.6