This commit is contained in:
Ryan6981 2024-10-22 14:10:15 +08:00
parent e62dfd86d4
commit 0e1f7eaaa9
19 changed files with 955 additions and 0 deletions

View File

@ -0,0 +1,11 @@
FROM rust:latest
WORKDIR /app
COPY api .
RUN cargo build --release
EXPOSE 8000
CMD ["cargo", "run", "--release"]

View File

@ -0,0 +1,11 @@
FROM rust:latest
WORKDIR /app
COPY web .
RUN cargo build --release
EXPOSE 80
CMD ["cargo", "run", "--release"]

122
online_compiler/REDME.md Normal file
View File

@ -0,0 +1,122 @@
README.md
markdown
深色版本
# 在线编译器项目
这是一个简单的在线编译器项目用户可以通过Web界面输入代码并选择编程语言然后执行代码并查看结果。该项目由两部分组成
- **API服务**负责接收代码执行请求使用Docker容器来执行代码并返回结果。
- **Web服务**:提供一个用户界面,允许用户输入代码并查看执行结果。
## 项目结构
online-compiler
├── api
│ ├── Cargo.toml
│ ├── src
│ │ └── main.rs
├── web
│ ├── Cargo.toml
│ ├── src
│ │ └── main.rs
│ ├── static
│ │ ├── styles.css
│ │ ├── scripts.js
│ │ └── index.html
├── Dockerfile.api
├── Dockerfile.web
└── docker-compose.yml
深色版本
## 快速开始
1. 确保你已经安装了[Docker](https://docs.docker.com/get-docker/)和[Docker Compose](https://docs.docker.com/compose/install/)。
2. 克隆此仓库到你的本地机器。
3. 进入项目根目录。
4. 使用以下命令启动项目:
```bash
docker-compose up --build
打开浏览器访问 http://localhost 来使用在线编译器。
API文档
执行代码
URL: /api/v1/execute
方法: POST
请求体 (JSON):
json
深色版本
{
"code": "print('Hello, world!')",
"language": "python"
}
响应 (JSON):
json
深色版本
{
"id": "b7c5f6a7-8d9a-4e00-9f5b-5a7c9f6b5d8e",
"status": "queued",
"response": null
}
id: 执行任务的唯一标识符。
status: 当前状态("queued", "compiling", "running", "completed")。
response: 如果代码执行完成则包含输出结果否则为null。
查询执行状态
URL: /api/v1/status/{id}
方法: GET
路径参数:
id: 执行任务的唯一标识符。
响应 (JSON):
json
深色版本
{
"id": "b7c5f6a7-8d9a-4e00-9f5b-5a7c9f6b5d8e",
"status": "completed",
"response": {
"stdout": "Hello, world!\n",
"stderr": "",
"returncode": 0
}
}
id: 执行任务的唯一标识符。
status: 当前状态("queued", "compiling", "running", "completed")。
response: 如果代码执行完成则包含输出结果否则为null。
示例
执行代码示例
bash
深色版本
curl -X POST http://localhost:8000/api/v1/execute \
-H "Content-Type: application/json" \
-d '{"code":"print(\\\"Hello, world!\\\")","language":"python"}'
查询状态示例
bash
深色版本
curl -X GET http://localhost:8000/api/v1/status/b7c5f6a7-8d9a-4e00-9f5b-5a7c9f6b5d8e

1
online_compiler/api/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

7
online_compiler/api/Cargo.lock generated Normal file
View File

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "api"
version = "0.1.0"

View File

@ -0,0 +1,21 @@
[package]
name = "online_compiler_api"
version = "0.1.0"
edition = "2021"
[dependencies]
hyper-tls = "0.6" # 确保这是最新的稳定版本
native-tls = "0.2" # 明确指定 0.2 版本
rocket = { version = "0.5", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = "0.8"
tempfile = "3.1"
docker = "0.0.40"
tokio = { version = "1.0", features = ["full"] }
lazy_static = "1.4"
reqwest = { version = "0.12", features = ["json"] }

View File

@ -0,0 +1,283 @@
#[macro_use] extern crate rocket;
extern crate serde;
#[macro_use] extern crate serde_derive;
extern crate tokio;
extern crate uuid;
extern crate tempfile;
extern crate docker;
extern crate lazy_static;
extern crate reqwest;
use rocket::response::content::Json;
use rocket::serde::{json::Json, Serialize, Deserialize};
use std::process::Command;
use std::fs;
use std::io::Write;
use tempfile::NamedTempFile;
use uuid::Uuid;
use docker::Docker;
use docker::opts::{HostConfig, ResourceConstraints};
use std::time::{Duration, Instant};
use tokio::time::timeout;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "rocket::serde")]
struct CodeRequest {
code: String,
language: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(crate = "rocket::serde")]
struct CodeResponse {
stdout: String,
stderr: String,
returncode: i32,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(crate = "rocket::serde")]
struct ExecutionStatus {
id: Uuid,
status: String,
response: Option<CodeResponse>,
}
// 定义一个结构体来存储容器信息
struct ContainerInfo {
container: docker::Container,
last_used: Instant,
}
// 定义一个全局的容器缓存
lazy_static! {
static ref CONTAINER_CACHE: Arc<Mutex<HashMap<String, ContainerInfo>>> = Arc::new(Mutex::new(HashMap::new()));
static ref CONTAINER_LIMIT: Arc<Mutex<usize>> = Arc::new(Mutex::new(0));
static ref EXECUTION_STATUS: Arc<Mutex<HashMap<Uuid, ExecutionStatus>>> = Arc::new(Mutex::new(HashMap::new()));
}
async fn execute_code(code: &str, language: &str, execution_id: Uuid) -> Result<CodeResponse, String> {
// 获取 Docker 客户端
let docker = Docker::connect_with_defaults().unwrap();
// 根据语言选择 Docker 镜像
let image_name = match language {
"python" => "python-runner",
"rust" => "rust-runner",
"nim" => "nim-runner",
"lua" => "lua-runner",
_ => return Err("Unsupported language".to_string()),
};
// 创建临时文件
let mut temp_file = NamedTempFile::new().unwrap();
write!(temp_file, "{}", code).unwrap();
let temp_path = temp_file.path().to_str().unwrap();
// 检查缓存中是否有可用的容器
let mut cache = CONTAINER_CACHE.lock().unwrap();
if let Some(container_info) = cache.get_mut(image_name) {
// 如果容器最近被使用过,则复用它
if container_info.last_used.elapsed() < Duration::from_secs(3600) { // 1小时
container_info.last_used = Instant::now();
let (status, output) = timeout(Duration::from_secs(600), async { // 10分钟超时
let status = container_info.container.wait(None).await.map_err(|e| e.to_string())?;
let logs = container_info.container.logs(true, true, None, None, None, None).await.map_err(|e| e.to_string())?;
(status, String::from_utf8(logs).unwrap())
}).await.map_or_else(
|_| {
Err("Execution timed out".to_string())
},
|result| result
)?;
// 解析输出
let (stdout, stderr) = if status.success {
(output, String::new())
} else {
(String::new(), output)
};
return Ok(CodeResponse {
stdout,
stderr,
returncode: status.exit_code.unwrap_or(-1) as i32,
});
}
}
// 创建新的容器
let host_config = HostConfig {
resources: Some(ResourceConstraints {
memory: Some(128 * 1024 * 1024), // 128MB 内存限制
cpu_shares: Some(1024), // CPU 资源限制
..Default::default()
}),
network_mode: Some("none".to_string()), // 禁用网络
..Default::default()
};
let container = docker.create_container(
Some(image_name),
vec!["/bin/sh", "-c", &format!("cat {} | {} -", temp_path, language)],
None,
Some(host_config),
None,
None,
None,
None,
None,
None,
None,
None,
).await.map_err(|e| {
error!("Failed to create container: {}", e);
"Failed to create container".to_string()
})?;
// 启动容器
container.start(None).await.map_err(|e| {
error!("Failed to start container: {}", e);
"Failed to start container".to_string()
})?;
// 设置超时时间为10分钟
let timeout_duration = Duration::from_secs(600);
let (status, output) = timeout(timeout_duration, async {
let status = container.wait(None).await.map_err(|e| e.to_string())?;
let logs = container.logs(true, true, None, None, None, None).await.map_err(|e| e.to_string())?;
(status, String::from_utf8(logs).unwrap())
}).await.map_or_else(
|_| {
Err("Execution timed out".to_string())
},
|result| result
)?;
// 将新的容器添加到缓存中
cache.insert(image_name.to_string(), ContainerInfo {
container: container.clone(),
last_used: Instant::now(),
});
// 增加创建容器的计数
let mut limit = CONTAINER_LIMIT.lock().unwrap();
*limit += 1;
// 检查是否超过每小时的最大创建数量
if *limit > 3 { // 每小时最多创建3个新容器
// 清理旧的容器
for (key, value) in cache.iter_mut() {
if value.last_used.elapsed() > Duration::from_secs(3600) {
value.container.remove(None).await.ok();
cache.remove(key);
}
}
*limit = 0; // 重置计数
}
// 解析输出
let (stdout, stderr) = if status.success {
(output, String::new())
} else {
(String::new(), output)
};
Ok(CodeResponse {
stdout,
stderr,
returncode: status.exit_code.unwrap_or(-1) as i32,
})
}
#[post("/api/v1/execute", format = "json", data = "<request>")]
async fn handle_execute_code(request: Json<CodeRequest>) -> Result<Json<ExecutionStatus>, rocket::response::status::Custom<String>> {
let code_request = request.into_inner();
let code = &code_request.code;
let language = &code_request.language;
let execution_id = Uuid::new_v4();
// 初始化执行状态
let mut status_map = EXECUTION_STATUS.lock().unwrap();
status_map.insert(execution_id, ExecutionStatus {
id: execution_id,
status: "queued".to_string(),
response: None,
});
// 异步执行代码
tokio::spawn(async move {
// 更新状态为“正在编译”
let mut status_map = EXECUTION_STATUS.lock().unwrap();
if let Some(status) = status_map.get_mut(&execution_id) {
status.status = "compiling".to_string();
}
let response = match execute_code(code, language, execution_id).await {
Ok(response) => Some(response),
Err(e) => {
error!("Execution failed: {}", e);
None
}
};
// 更新执行状态
let mut status_map = EXECUTION_STATUS.lock().unwrap();
if let Some(status) = status_map.get_mut(&execution_id) {
status.status = "completed".to_string();
status.response = response;
}
});
// 返回初始状态
Ok(Json(ExecutionStatus {
id: execution_id,
status: "queued".to_string(),
response: None,
}))
}
#[get("/api/v1/status/<id>")]
async fn get_execution_status(id: Uuid) -> Json<ExecutionStatus> {
let status_map = EXECUTION_STATUS.lock().unwrap();
if let Some(status) = status_map.get(&id) {
Json(status.clone())
} else {
Json(ExecutionStatus {
id,
status: "not found".to_string(),
response: None,
})
}
}
#[get("/api/v1/result/<id>")]
async fn get_execution_result(id: Uuid) -> Json<CodeResponse> {
let status_map = EXECUTION_STATUS.lock().unwrap();
if let Some(status) = status_map.get(&id) {
if let Some(response) = &status.response {
Json(response.clone())
} else {
Json(CodeResponse {
stdout: "".to_string(),
stderr: "Result not available".to_string(),
returncode: -1,
})
}
} else {
Json(CodeResponse {
stdout: "".to_string(),
stderr: "Execution ID not found".to_string(),
returncode: -1,
})
}
}
#[launch]
fn rocket() -> _ {
rocket::build()
.mount("/", routes![handle_execute_code, get_execution_status, get_execution_result])
}

View File

@ -0,0 +1,24 @@
version: '3'
services:
api:
build:
context: .
dockerfile: Dockerfile.api
ports:
- "8000:8000"
networks:
- app-network
web:
build:
context: .
dockerfile: Dockerfile.web
ports:
- "8001:8001"
networks:
- app-network
depends_on:
- api
networks:
app-network:

1
online_compiler/web/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

View File

@ -0,0 +1,12 @@
[package]
name = "online_compiler_web"
version = "0.1.0"
edition = "2021"
[dependencies]
rocket = { version = "0.5", features = ["json"] }
rocket_contrib = { version = "0.4.10", features = ["serve"] }
rocket_cors = "0.5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.12", features = ["json"] }

View File

@ -0,0 +1,145 @@
#[macro_use] extern crate rocket;
#[macro_use] extern crate rocket_contrib;
use rocket::response::content::Html;
use rocket_contrib::serve::StaticFiles;
use rocket_cors::{AllowedOrigins, CorsOptions};
use rocket::State;
use rocket::request::Form;
use rocket::http::Status;
use serde_json::{json, Value};
use reqwest::Client;
use std::sync::Mutex;
// 用于存储API客户端
struct ApiClient {
client: Client,
}
impl ApiClient {
fn new() -> Self {
ApiClient {
client: Client::new(),
}
}
async fn execute_code(&self, code: &str, language: &str) -> Result<Value, String> {
let url = "http://api:8000/api/v1/execute"; // 使用Docker网络中的服务名
let body = json!({
"code": code,
"language": language
});
let response = self.client.post(url)
.json(&body)
.send()
.await
.map_err(|e| e.to_string())?;
if response.status().is_success() {
response.json::<Value>().await.map_err(|e| e.to_string())
} else {
Err("API request failed".to_string())
}
}
async fn get_status(&self, id: &str) -> Result<Value, String> {
let url = format!("http://api:8000/api/v1/status/{}", id);
let response = self.client.get(&url)
.send()
.await
.map_err(|e| e.to_string())?;
if response.status().is_success() {
response.json::<Value>().await.map_err(|e| e.to_string())
} else {
Err("API request failed".to_string())
}
}
}
#[get("/")]
fn index() -> Html<&'static str> {
Html(r#"
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>线</title>
<link rel="stylesheet" href="/static/styles.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<div class="container">
<h1>线</h1>
<div class="editor-container">
<textarea id="code" placeholder="在这里输入你的代码" required></textarea>
<select id="language" required>
<option value="python">Python</option>
<option value="rust">Rust</option>
<option value="nim">Nim</option>
<option value="lua">Lua</option>
</select>
<button type="button" id="run-button"></button>
</div>
<div class="output-container">
<div id="status"></div>
<pre id="output"></pre>
</div>
</div>
<script src="/static/scripts.js"></script>
</body>
</html>
"#)
}
#[post("/run", data = "<form>")]
async fn run_code(form: Form<CodeForm>, api_client: State<Mutex<ApiClient>>) -> Result<rocket::response::Json<Value>, Status> {
let form_data = form.into_inner();
let code = &form_data.code;
let language = &form_data.language;
let api_client = api_client.lock().unwrap();
match api_client.execute_code(code, language).await {
Ok(response) => Ok(rocket::response::Json(response)),
Err(e) => {
error!("API call failed: {}", e);
Err(Status::InternalServerError)
}
}
}
#[get("/api/v1/status/<id>")]
async fn get_execution_status(id: String, api_client: State<Mutex<ApiClient>>) -> Result<rocket::response::Json<Value>, Status> {
let api_client = api_client.lock().unwrap();
match api_client.get_status(&id).await {
Ok(status) => Ok(rocket::response::Json(status)),
Err(e) => {
error!("API call failed: {}", e);
Err(Status::InternalServerError)
}
}
}
#[derive(FromForm)]
struct CodeForm {
code: String,
language: String,
}
#[launch]
fn rocket() -> _ {
let cors = CorsOptions::default()
.allowed_origins(AllowedOrigins::all())
.to_cors().unwrap();
rocket::build()
.mount("/", routes![index])
.mount("/static", StaticFiles::from("static/"))
.manage(Mutex::new(ApiClient::new()))
.mount("/run", routes![run_code])
.mount("/api/v1/status", routes![get_execution_status])
.attach(cors)
.port(8001) // 在这里指定监听 8001 端口
}

View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>在线编译器</title>
<link rel="stylesheet" href="styles.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<div class="container">
<h1>在线编译器</h1>
<div class="editor-container">
<div class="language-selector">
<label for="language">选择语言:</label>
<select id="language" required>
<option value="python">Python</option>
<option value="rust">Rust</option>
<option value="nim">Nim</option>
<option value="lua">Lua</option>
</select>
</div>
<textarea id="code" placeholder="在这里输入你的代码" required></textarea>
<button type="button" id="run-button">运行代码</button>
</div>
<div class="output-container">
<div id="status"></div>
<pre id="output"></pre>
</div>
</div>
<script src="scripts.js"></script>
</body>
</html>

View File

@ -0,0 +1,53 @@
$(document).ready(function() {
$('#run-button').on('click', function() {
const code = $('#code').val();
const language = $('#language').val();
// 初始化状态
$('#status').text('排队中...');
$('#output').text('');
$.ajax({
url: '/run',
method: 'POST',
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
data: { code, language },
success: function(response) {
const executionId = response.id;
pollForStatus(executionId);
},
error: function(error) {
console.error('Error:', error);
$('#status').text('请求失败');
$('#output').text('无法连接到服务器,请稍后再试。');
}
});
});
function pollForStatus(executionId) {
const interval = setInterval(() => {
$.ajax({
url: `/api/v1/status/${executionId}`,
method: 'GET',
success: function(response) {
$('#status').text(`状态: ${response.status}`);
if (response.status === 'completed') {
clearInterval(interval);
if (response.response) {
$('#output').text(`输出:\n${response.response.stdout}\n\n错误:\n${response.response.stderr}`);
} else {
$('#output').text('结果不可用。');
}
} else if (response.status === 'compiling' || response.status === 'running') {
$('#status').text(`状态: ${response.status}`);
}
},
error: function(error) {
console.error('Error:', error);
$('#status').text('请求失败');
$('#output').text('无法连接到服务器,请稍后再试。');
}
});
}, 1000); // 每秒轮询一次
}
});

View File

@ -0,0 +1,55 @@
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
.container {
max-width: 800px;
margin: 50px auto;
padding: 20px;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border-radius: 5px;
}
.editor-container, .output-container {
margin-bottom: 20px;
}
.language-selector {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.language-selector label {
margin-right: 10px;
}
textarea {
width: 100%;
height: 200px;
padding: 10px;
font-size: 14px;
border: 1px solid #ddd;
border-radius: 5px;
}
select, button {
padding: 10px;
font-size: 14px;
border: 1px solid #ddd;
border-radius: 5px;
cursor: pointer;
}
.output-container pre {
white-space: pre-wrap;
word-wrap: break-word;
background-color: #f9f9f9;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}

145
online_runer/REDME. md Normal file
View File

@ -0,0 +1,145 @@
## 安装与运行
### 前提条件
- 安装 [Rust](https://www.rust-lang.org/tools/install)
- 安装 [Docker](https://docs.docker.com/get-docker/)
- 安装 [Docker Compose](https://docs.docker.com/compose/install/)
### 构建Docker镜像
你说得对为了使在线编译器API能够正常工作我们需要确保依赖的Docker镜像环境已经构建好并推送到Docker Hub。这些镜像将用于执行不同语言的代码。
1. 构建和推送Docker镜像
首先我们需要为每种编程语言创建一个Docker镜像并将其推送到Docker Hub。假设你已经有了Docker Hub账号以下是每种语言的Dockerfile示例
Python Runner Dockerfile (python-runner/Dockerfile)
dockerfile
深色版本
FROM python:3.9-slim
# 设置工作目录
WORKDIR /code
# 安装必要的包
RUN pip install --no-cache-dir -q flake8
# 默认命令
CMD ["python", "-"]
Rust Runner Dockerfile (rust-runner/Dockerfile)
dockerfile
深色版本
FROM rust:latest
# 设置工作目录
WORKDIR /code
# 默认命令
CMD ["rustc", "-", "-o", "/code/a.out" && "./a.out"]
Nim Runner Dockerfile (nim-runner/Dockerfile)
dockerfile
深色版本
FROM nimlang/nim:latest
# 设置工作目录
WORKDIR /code
# 默认命令
CMD ["nim", "compile", "-", "&&", "./a.out"]
Lua Runner Dockerfile (lua-runner/Dockerfile)
dockerfile
深色版本
FROM lua:5.4
# 设置工作目录
WORKDIR /code
# 默认命令
CMD ["lua", "-"]
2. 构建并推送Docker镜像
对于每个Dockerfile你需要在相应的目录中运行以下命令来构建和推送镜像到Docker Hub。请确保你已经登录到Docker Hub。
构建并推送Python Runner镜像
sh
深色版本
cd python-runner
docker build -t your_dockerhub_username/python-runner .
docker push your_dockerhub_username/python-runner
构建并推送Rust Runner镜像
sh
深色版本
cd rust-runner
docker build -t your_dockerhub_username/rust-runner .
docker push your_dockerhub_username/rust-runner
构建并推送Nim Runner镜像
sh
深色版本
cd nim-runner
docker build -t your_dockerhub_username/nim-runner .
docker push your_dockerhub_username/nim-runner
构建并推送Lua Runner镜像
sh
深色版本
cd lua-runner
docker build -t your_dockerhub_username/lua-runner .
docker push your_dockerhub_username/lua-runner
1. 构建并推送每种语言的执行环境到Docker Hub
- Python Runner:
```sh
cd python-runner
docker build -t your_dockerhub_username/python-runner .
docker push your_dockerhub_username/python-runner
```
- Rust Runner:
```sh
cd rust-runner
docker build -t your_dockerhub_username/rust-runner .
docker push your_dockerhub_username/rust-runner
```
- Nim Runner:
```sh
cd nim-runner
docker build -t your_dockerhub_username/nim-runner .
docker push your_dockerhub_username/nim-runner
```
- Lua Runner:
```sh
cd lua-runner
docker build -t your_dockerhub_username/lua-runner .
docker push your_dockerhub_username/lua-runner
```
2. 构建主项目Docker镜像
```sh
docker-compose build

View File

@ -0,0 +1,7 @@
FROM lua:5.4
# 设置工作目录
WORKDIR /code
# 默认命令
CMD ["lua", "-"]

View File

@ -0,0 +1,7 @@
FROM nimlang/nim:latest
# 设置工作目录
WORKDIR /code
# 默认命令
CMD ["nim", "compile", "-", "&&", "./a.out"]

View File

@ -0,0 +1,10 @@
FROM python:3.9-slim
# 设置工作目录
WORKDIR /code
# 安装必要的包
RUN pip install --no-cache-dir -q flake8
# 默认命令
CMD ["python", "-"]

View File

@ -0,0 +1,7 @@
FROM rust:latest
# 设置工作目录
WORKDIR /code
# 默认命令
CMD ["rustc", "-", "-o", "/code/a.out" && "./a.out"]