🚀 Runcode 在线代码运行编辑器

1,444 阅读4分钟

线上地址: rucode

github: github

PC 端

image.png

移动端

image.png

前言

1.js 外的语言学习

刷题时边学习了几门语言,学习一些语法时有在线编辑器的需求

image.png

用过的几个在线编辑器

语言支持较少,运行频率有很大限制,运行速度比较快,支持输入,无代码格式化

运行频率无限制,运行速度快,但是不支持输入,无代码格式化

做的比较专业,运行速度快,无频率限制,强制登录解锁输入,支持依赖安装,代码格式化支持部分语言

支持语言多,单语言多版本支持也多,运行速度比较快,编辑器高亮支持不全,无代码格式化

总的而言,对于在线编辑器分为几点要求

  • 语言支持多
  • 单语言版本支持多
  • 运行速度快
  • 无频率限制
  • 代码高亮支持
  • 代码格式化支持
  • 本地保存
  • 支持输入

于是,便踏上了自己做个应用的想法。。。

服务端

环境隔离

前置条件, 安装 docker

windows 安装

刚开始装了个 GCC 的镜像,跑了下 C++ 后感觉能行,另外 docker 能做到环境隔离

server

本来想用 Go 写服务端的, 想到服务端不会有太多时,操作 docker 运行代码即可,但是边学边写怕是年底都上不了

转头想还是 Nodejs,写了几天写完了第一个版本

在 github 找了个 dockerode 库,对在 Nodejs 中操作 docker 进行了一层封装

提前写好 Dockerfile 文件,示例 GCC

image.png

创建镜像 直接使用命令

docker build -t gcc:11

想过 使用 zx 写脚本,不过中途报错了,没管,后续整上

#!/usr/bin/env zx
const path = require('path');

const root = path.resolve('.');

const dockerPath = 'src/docker';

const nodejs = path.join(root, dockerPath, 'nodejs');

cd(nodejs);
await $`docker build -t nodejs:lts .`;

cd(root);
await $`docker build -t cpp:11 .`;

// await $`docker build -t go:latest ../src/docker/go`;

关于 docker 的使用,我看了这篇教程 Docker 从入门到实践

调试镜像,进入容器试运行,启动一个 bash 终端,进行交互

docker run -t -i gcc:11 /bin/sh

可以提前写好 C++ 测试代码文件,创建 code.cpp 文件,写入以下内容

code.cpp 文件内容

#include <iostream>

int main() {
  std::cout << "hello world" << std::endl;
  return 0;
}

另起终端,copy 进 docker,直接放 根目录

# mycontainer 是你正在运行的 container id, docker Desktop 可以查看当前运行的容器 id
docker cp ./code.cpp mycontainer:/

在 docker 容器终端编译运行,会输出 hello world,demo 验证结束

# 编译
g++ code.cpp -o code.out
# 运行
./code.out

API 服务

  • Koa

image.png

  • koa-router

做好入参校验

image.png

调起 docker 并运行代码

详情 查看 dockerode 文档

  1. 提前定义好每种语言运行的指令

image.png

  1. 将代码写入 docker

这里没有将代码写入文件 copy 进 docker ,而是直接使用 cat 命令写进 docker 中, 标准输入也写入

image.png

  1. 创建容器

输入运行代码的 bash 命令,禁用容器网络,容器创建成功定义 移除方法

image.png

  1. 获取终端输出

image.png

  1. 格式化输出

image.png

剔除了终端转义字符,在 浏览器端 识别不了,不过后期可以将这些字符(大部分是颜色代码)输出到浏览器端

剔除超出长度的部分

image.png

  1. 超出运行时间,终止并移除 container,超时机制
const timeoutSig = setTimeout(handleOutput, DockerRunConfig.timeout);
  1. 监听运行情况,结束后直接处理输出
container?.wait((status) => {
    if (!status || status?.Status === DockerRunStatus.exited) {
      clearTimeout(timeoutSig);
      handleOutput();
    }
  });
  1. 标准输入的处理
  • C++
g++ code.cpp -o code.out && ./code.out < input.txt
  1. 服务端处理结束,增加编程语言需要去 docker hub 找对应镜像,或者自己做一个,目前使用的 官方镜像比较大,1 GB 左右

客户端

编辑器

自己撸一个也可以,但是没必要,咱做的是应用,直接使用现成的成熟的编辑器

monaco editor vs codemirror

  1. 初始化 monaco editor,并保存示例
useEffect(() => {
    if (monacoRef.current) {
      const codeCache = storage.get(CodeStorageKey[type]);
      editorRef.current = monaco.editor.create(monacoRef.current, {
        value: codeCache || template[type],
        language: languageMap[type],
        theme: themeType,
        formatOnType: true,
        smoothScrolling: true,
        formatOnPaste: true,
        readOnly: false,
      });

      return () => editorRef.current?.dispose();
    }
  }, [type, themeType]);
  1. 代码补全以及错误提示

monaco 实现了 ts/js 语言的,采用worker的方式,语法解析需要耗费大量时间,使用worker来异步处理返回结果

import * as monaco from 'monaco-editor';
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';

// @ts-ignore
self.MonacoEnvironment = {
  getWorker(_: any, label: string) {
    if (label === 'typescript' || label === 'javascript') {
      return new tsWorker();
    }
    return new editorWorker();
  },
};

monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true);

daisyUI

纯 CSS 组件库,风格清新简约,使用简单

在其上包装了一层,暴露统一接口出去,使用起来跟其他组件库无异

状态管理

mobx + mobx-persist-store 持久化

  • 存设置、语言选择、主题等
  • 管理 Toast

代码格式化

monaco 支持 Js 的格式化

其他语言的在 github 找到个 clang-format 的库,支持 C/C++/Java 等语言,作者编译成了 wasm 模块

Clang In Browser (cib)

自己编译的话需要费点时间,5 年未更新,有些仓库分支有更新,留意 PR

包装成 hook 使用

function useClangFormat({ onCodeFormatDone }: Props) {
  const [clangFormat, setClangFormat] = useState<ProcessManager>();

  const clangFormatRef = useRef<ProcessManager>();

  useEffect(() => {
    let clangFormat = new ProcessManager('clang-format', 'clang-format');

    clangFormat.start();
    clangFormat.workerReady = () => {
      setClangFormat(clangFormat);
    };

    // @ts-ignore
    clangFormat.workerFormatDone = (args) => {
      onCodeFormatDone(args?.result);
    };

    clangFormatRef.current = clangFormat;
  }, []);
  return [clangFormat];
}

其他 语言的需要在找找

自动化构建部署

利用 github action 做构建部署,账号信息 保存到 action secret 中

  1. 客户端
  • main 分支 client 文件下有变更触发构建部署

  • 构建代码

  • 往 oss 推文件

  • 往 服务器推文件

  • 重启 nginx

# This is a basic workflow to help you get started with Actions

name: CI

# Controls when the action will run.
on:
  # Triggers the workflow on push or pull request events but only for the master branch
  push:
    branches: [main]
    paths:
      - 'client/**'
      - '!client/config/nginx.conf'

  pull_request:
    branches: [main]
    paths:
      - 'client/**'
      - '!client/config/nginx.conf'

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # This workflow contains a single job called "build"
  build-client:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest

    defaults:
      run:
        working-directory: ./client

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
      - uses: actions/checkout@v2

      - name: Use Node.js 14.17.0
        uses: actions/setup-node@v1
        with:
          node-version: 14.17.0

      - name: Install pnpm
        run: npm i pnpm -g

      - name: Install Packages
        run: pnpm install

      - name: Build
        run: pnpm build

      # cdn 部署
      - name: Deploy static source
        run: pnpm cdn ${{ secrets.ACCESS_KEY_ID }} ${{ secrets.ACCESS_KEY_SECRET }} ${{ secrets.OSS_BUCKET }} ${{ secrets.OSS_REGION }}

      # 移除 部署 cdn 的文件
      - name: remove static file which is deploy cdn
        run: rm -rf dist/assets

      # 推到服务器
      - name: Deploy to Server # 第二步,rsync推文件
        uses: AEnterprise/rsync-deploy@v1.0 # 使用别人包装好的步骤镜像
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} # 引用配置,SSH私钥
          ARGS: -avz --delete --exclude='*.pyc' # rsync参数,排除.pyc文件
          SERVER_PORT: ${{ secrets.SSH_PORT }} # SSH端口
          FOLDER: ./client/dist # 要推送的文件夹,路径相对于代码仓库的根目录
          SERVER_IP: ${{ secrets.SSH_HOST }} # 引用配置,服务器的host名(IP或者域名domain.com)
          USERNAME: ${{ secrets.SSH_USER }} # 引用配置,服务器登录名
          SERVER_DESTINATION: /root/project/runcodeclient # 部署到目标文件夹

      # nginx 重启
      - name: Restart server # 第三步,重启服务
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SSH_HOST }} # 下面三个配置与上一步类似
          username: ${{ secrets.SSH_USER }}
          port: ${{ secrets.SSH_PORT }} # SSH端口
          key: ${{ secrets.DEPLOY_KEY }}
          # 重启的脚本,根据自身情况做相应改动,一般要做的是migrate数据库以及重启服务器
          script: |
            nginx -s reload

服务端

main 分支 server 文件夹有变动触发构建部署

  • 往服务器推文件

  • 进服务器 编译代码

  • kill 掉服务,重启

# This is a basic workflow to help you get started with Actions

name: CI

# Controls when the action will run.
on:
  # Triggers the workflow on push or pull request events but only for the master branch
  push:
    branches: [main]
    paths:
      - 'server/**'
      - '!server/config/nginx.conf'
  pull_request:
    branches: [main]
    paths:
      - 'server/**'
      - '!server/config/nginx.conf'

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  build-server:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest

    defaults:
      run:
        working-directory: ./server

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
      - uses: actions/checkout@v2

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}

      - name: Deploy to Server # 第二步,rsync推文件
        uses: AEnterprise/rsync-deploy@v1.0 # 使用别人包装好的步骤镜像
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} # 引用配置,SSH私钥
          ARGS: -avz --delete --exclude='*.pyc' # rsync参数,排除.pyc文件
          SERVER_PORT: ${{ secrets.SSH_PORT }} # SSH端口
          FOLDER: ./server # 要推送的文件夹,路径相对于代码仓库的根目录
          SERVER_IP: ${{ secrets.SSH_HOST }} # 引用配置,服务器的host名(IP或者域名domain.com)
          USERNAME: ${{ secrets.SSH_USER }} # 引用配置,服务器登录名
          SERVER_DESTINATION: /root/project/runcodeserver/ # 部署到目标文件夹

      - name: Restart server # 第三步,重启服务
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SSH_HOST }} # 下面三个配置与上一步类似
          username: ${{ secrets.SSH_USER }}
          port: ${{ secrets.SSH_PORT }} # SSH端口
          key: ${{ secrets.DEPLOY_KEY }}
          # 重启的脚本,根据自身情况做相应改动,一般要做的是migrate数据库以及重启服务器
          script: |
            cd project/runcodeserver/server
            pnpm install 
            pnpm build
            pnpm kill
            pnpm run deploy
            nginx -s reload

nginx 文件变动的更新

监听两个 nginx 配置文件的变动

  • 替换服务器 nginx 配置文件

  • 重启 nginx

# This is a basic workflow to help you get started with Actions

name: CI

# Controls when the action will run.
on:
  # Triggers the workflow on push or pull request events but only for the master branch
  push:
    branches: [main]
    paths:
      - 'client/config/nginx.conf'
  pull_request:
    branches: [main]
    paths:
      - 'client/config/nginx.conf'

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # This workflow contains a single job called "build"
  build-client:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest

    defaults:
      run:
        working-directory: ./client

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
      - uses: actions/checkout@v2

      - name: Deploy to Server # 第二步,rsync推文件
        uses: AEnterprise/rsync-deploy@v1.0 # 使用别人包装好的步骤镜像
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} # 引用配置,SSH私钥
          ARGS: -avz --delete --exclude='*.pyc' # rsync参数,排除.pyc文件
          SERVER_PORT: ${{ secrets.SSH_PORT }} # SSH端口
          FOLDER: ./client/config # 要推送的文件夹,路径相对于代码仓库的根目录
          SERVER_IP: ${{ secrets.SSH_HOST }} # 引用配置,服务器的host名(IP或者域名domain.com)
          USERNAME: ${{ secrets.SSH_USER }} # 引用配置,服务器登录名
          SERVER_DESTINATION: /root/project/runcodeclient/ # 部署到目标文件夹

      - name: Restart server # 第三步,重启服务
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SSH_HOST }} # 下面三个配置与上一步类似
          username: ${{ secrets.SSH_USER }}
          port: ${{ secrets.SSH_PORT }} # SSH端口
          key: ${{ secrets.DEPLOY_KEY }}
          # 重启的脚本,根据自身情况做相应改动,一般要做的是migrate数据库以及重启服务器
          script: |
            mv -f project/runcodeclient/config/nginx.conf /etc/nginx/conf.d/runcodec.conf
            systemctl restart nginx

做完这些,main 分支推代码 稍等 3 分钟即到线上

展望

后续会持续更新功能与修复 bug

  • 格式化支持度目前 50%
  • 语言覆盖不够
  • 标准输入目前只支持 C++