基于nestjs框架,使用dockerode运行代码,打造在线代码编译平台!

基于nestjs框架,使用dockerode运行代码,打造在线编译平台

dockerode命名由来:官方自称docker + node于是就命名docker + ode = dockerode (Node.js module for Docker’s Remote API)有点意思

成果:轻AI | 一站式AI对话与在线编程平台

需求:在线运行代码

思路:利用dockerode创建docker容器,运行代码,如果是py代码就基于python镜像,如果是java代码就基于openjdk镜像,以此类推。

例如——运行python代码关于dockerode的思路如下(假设docker是dockerode构造的实例):

1、检查python镜像是否存在

const image = docker.getImage(imageName);
await image.inspect();

2、不存在则基于python镜像build一个自己的镜像,存在则跳过

await checkImage() ? createImage() : ''

3、检查镜像所对因的容器是否存在

const containers = await this.docker.listContainers({
  filters: {
    name: [containerName]
  }
})
containers.find(c => {
  return c.Names.includes('/' + containerName)
})

4、容器不存在则创建、存在则跳过

await checkContainer() ? createContainer() : ''

5、检查容器是否为运行状态,没运行则运行

6、将代码copy进容器内部

container.exec({
    Cmd: ['sh', '-c', `cat > /app/文件名`],
});
const back = await exec.start({
    hijack: true,
    stdin: true,
});
back.write(fs.readFileSync(tmpFile).toString());

7、容器内部执行代码

container.exec({
    Cmd: ['python', `./文件名`],
})

8、获取代码执行结果(包括报错信息)

以上均为伪代码,主要为了提供思路

为了加快在线代码运行速度,所以镜像和容器一旦创建不会删除,所有要检查是不是存在。

整理好思路,开始制作功能

首页下载dockerode,创建一个docker实例,定义一个docker.providers.ts用于提供实例

// docker.providers.ts
import Docker from 'dockerode';
import fs from 'fs'

export const dockerProviders = [
  {
    provide: 'DOCKER_INSTANCE',
    useFactory: async () => {
      try {
        const docker = new Docker({
          host: 'http://localhost',
          port: 2375,
          version: 'v1.47', // required when Docker >= v1.13 —— 命令行docker version 查看Docker Api Version,
          // 如果开启https,需要填写证书
          // ca: fs.readFileSync('src/common/certs/ca.pem'),
          // cert: fs.readFileSync('src/common/certs/cert.pem'),
          // key: fs.readFileSync('src/common/certs/key.pem'),
        });
        // 检查是否连接成功
        // const info = await docker.info() 
        // console.log(info);
        return docker;
      } catch (error) {
        console.error('Failed to create Docker instance:', error);
        throw error;
      }
    },
  },
];

使用nest代码生成器创建一个docker模块

运行nest g resource docker会创建出一个docker module

引入docker实例

// docker.module.ts
import { Module } from '@nestjs/common';
import { DockerService } from './docker.service';
import { DockerController } from './docker.controller';
import { dockerProviders } from 'src/config/docker/docker.providers';

@Module({
  controllers: [DockerController],
  providers: [DockerService, ...dockerProviders],
  exports: [DockerService]
})
export class DockerModule { }

接下来docker.service.ts就可以注入实例了

// docker.service.ts
constructor(
    @Inject('DOCKER_INSTANCE')
    private docker: Docker,
) { }

紧接着进入本文章主要内容

先定义一个实体类ExecCode,包括源代码,输入,代码类型

// types/index.ts
export enum CodeType {
  JAVA = 'java',
  CPP = 'cpp',
  PYTHON = 'python'
}

// exec-code.entity.ts
import { CodeType } from 'src/types';
export class ExecCode {
  codeType: CodeType;
  sourceCode: string;
  input: string;
}

编写一个switch方法,通过代码类型,判断具体的执行逻辑

// docker.service.ts
execCode(execCode: ExecCode) {
    switch (execCode.codeType) {
      case CodeType.JAVA:
        this.execJava(execCode);
        break;
      case CodeType.PYTHON:
        this.execPython(execCode);
        break;
      case CodeType.CPP:
        this.execGcc(execCode)
        break
    }
}

先看看execPython的具体逻辑

npm i tmp (没有tmp库的安装)

现在docker环境中安装python镜像,否则构建会比较缓慢

docker pull python

推荐一个镜像加速站,非常好用!DockerHub镜像加速 | 运维开发绿皮书

async execPython(execCode: ExecCode, client: Socket) {
  const dir = tmp.dirSync({});// 创建临时文件夹
  const tmpFile = tmp.fileSync({ // 在临时文件夹中创建临时文件(对应要运行的代码文件,例如main.py)
    dir: dir.name,
    name: 'main.' + execCode.codeType,
  });
  fs.writeFileSync(tmpFile.name, execCode.sourceCode);// 将源代码写入main.py
  // Dockerfile内容,用于构建镜像
  const dockerfileContent = `
    FROM python
    COPY ${path.basename(tmpFile.name)} /app/
    WORKDIR /app
  `;
  // 创建Dockerfile
  const dockerfileTmp = tmp.fileSync({
    dir: dir.name,
    name: 'Dockerfile',
  });
  // 写入内容
  fs.writeFileSync(dockerfileTmp.name, dockerfileContent);
  // 检查镜像,没有就创建,CoderRunnerConstant是常量类,在文章下提供
  if (
    !(await this.checkImage(CoderRunnerConstant.PYTHON_RUNNER_IMAGE_NAME))
  ) {
    await this.createImage(
      CoderRunnerConstant.PYTHON_RUNNER_IMAGE_NAME,
      dockerfileTmp,
      tmpFile,
    );
  }
  try {
    // 检查容器是否存在,没有就创建
    let container: Docker.Container | null = await this.checkContainer(CoderRunnerConstant.PYTHON_CONTAINER_NAME)
    if (!container) {
      container = await this.createContainer(CoderRunnerConstant.PYTHON_RUNNER_IMAGE_NAME, CoderRunnerConstant.PYTHON_CONTAINER_NAME)
    }
    // 经检查容器是否正在运行,没有则允许
    await this.checkStatusAndStart(container)
    // 将main.py复制进容器
    await this.copyFileContent(container, tmpFile);
    // 容器执行命令
    const exec = await container.exec({
      Cmd: ['python', `./${path.basename(tmpFile.name)}`],
      AttachStdin: true,
      AttachStdout: true,
      AttachStderr: true,
    });
    // hijack开启伪控制台,可以输入内容
    exec.start({ hijack: true, stdin: true }, function (err, stream) {
      if (err) return console.error(err);
      // 控制台写入输入
      stream?.write(execCode.input);
      stream?.end();
      // 正常输出和错误输出分离
      const stdout = new PassThrough();
      const stderr = new PassThrough();
      exec.modem.demuxStream(stream, stdout, stderr);
      stdout?.on('data', (chunk) => {
        console.log(chunk.toString())
      });
      stderr?.on('data', (chunk) => {
        console.log(chunk.toString())
      });

      stream?.on('end', async function () {
        console.log('执行完毕')
      });
    });
  } catch (error) {
    console.log(error.message);
    console.log('运行出错')
  } finally {
    tmpFile.removeCallback();
    dockerfileTmp.removeCallback();
    dir.removeCallback();
  }
}

检查镜像函数实现

async checkImage(imageName: string) {
  try {
    const image = this.docker.getImage(imageName);
    await image.inspect();
    return true;
  } catch (error) {
    console.log(`${imageName}不存在,正在构建`);
    return false;
  }
}

创建镜像函数实现

async createImage(
  imageName: string,
  dockerfileTmp: tmp.FileResult,
  tmpFile: tmp.FileResult,
) {
  try {
    let stream = await this.docker.buildImage(
      {
        context: path.dirname(dockerfileTmp.name),
        src: [path.basename(dockerfileTmp.name), path.basename(tmpFile.name)],
      },
      { t: imageName },
    );
    stream.on('data', (chunk) => {
      console.log(chunk.toString());
    });
    return new Promise((resolve, reject) => {
      this.docker.modem.followProgress(stream, (err, res) => {
        if (err) {
          console.error('Error building image:', err);
          reject(err);
        }
        resolve(res);
      });
    });
  } catch (error) {
    console.log('构建镜像出错:', error.message);
  }
}

检查容器函数实现

async checkContainer(containerName: string) {
  const containers = await this.docker.listContainers({
    filters: {
      name: [containerName]
    }
  })
  const containerInfo = containers.find(c => {
    return c.Names.includes('/' + containerName)
  })
  if (!containerInfo) return null
  return this.docker.getContainer(containerInfo.Id)
}

创建容器函数实现

async createContainer(imaegName: string, containerName: string) {
  return await this.docker.createContainer({
    Image: imaegName,
    Cmd: ['sh', '-c', 'while true; do sleep 1; done'],
    Tty: false,
    name: containerName
  });
}

容器运行函数实现

async checkStatusAndStart(container: Docker.Container) {
  const inspect = await container.inspect()
  if (inspect.State.Status !== 'running') {
    await container.start()
  }
}

拷贝文件内容函数实现

async copyFileContent(container: Docker.Container, tmpFile: tmp.FileResult) {
  try {
    const exec = await container.exec({
      Cmd: ['sh', '-c', `cat > /app/${path.basename(tmpFile.name)}`],
      AttachStdin: true,
      AttachStdout: true,
      AttachStderr: true,
    });
    const back = await exec.start({
      hijack: true,
      stdin: true,
    });
    back.write(fs.readFileSync(tmpFile.name).toString());
    back.end();
  } catch (error) {
    console.log('拷贝文件失败:' + error);
  }
}

执行java和cpp的代码同理,不过最好也先在docker环境中拉取好对应的镜像openjdk:22和gcc

docker pull openjdk:22

docker pull gcc

async execJava(execCode: ExecCode) {
  const dir = tmp.dirSync({});
  const tmpFile = tmp.fileSync({
    dir: dir.name,
    name: 'main.' + execCode.codeType,
  });
  fs.writeFileSync(tmpFile.name, execCode.sourceCode);
  const dockerfileContent = `
    FROM openjdk:22
    COPY ${path.basename(tmpFile.name)} /app/
    WORKDIR /app
  `;
  const dockerfileTmp = tmp.fileSync({
    dir: dir.name,
    name: 'Dockerfile',
  });
  fs.writeFileSync(dockerfileTmp.name, dockerfileContent);
  if (!(await this.checkImage(CoderRunnerConstant.JAVA_RUNNER_IMAGE_NAME))) {
    await this.createImage(
      CoderRunnerConstant.JAVA_RUNNER_IMAGE_NAME,
      dockerfileTmp,
      tmpFile,
    );
  }
  try {
    let container: Docker.Container | null = await this.checkContainer(CoderRunnerConstant.JAVA_CONTAINER_NAME)
    if (!container) {
      container = await this.createContainer(CoderRunnerConstant.JAVA_RUNNER_IMAGE_NAME, CoderRunnerConstant.JAVA_CONTAINER_NAME)
    }
    await this.checkStatusAndStart(container)
    await this.copyFileContent(container, tmpFile);
    const exec = await container.exec({
      Cmd: ['java', `./${path.basename(tmpFile.name)}`],
      AttachStdin: true,
      AttachStdout: true,
      AttachStderr: true,
    });
    exec.start({ hijack: true, stdin: true, Tty: false }, (err, stream) => {
      if (err) return console.error(err);
      stream?.pipe(process.stderr)
      stream?.write(execCode.input);
      stream?.end();
      const stdout = new PassThrough();
      const stderr = new PassThrough();
      exec.modem.demuxStream(stream, stdout, stderr);
      stdout?.on('data', (chunk) => {
        console.log(chunk.toString())
      });
      stderr?.on('data', (chunk) => {
        console.log(chunk.toString())
      });
      stream?.on('end', async function () {
        console.log('执行完成')
      });
    });
  } catch (error) {
    console.log('执行容器出错:', error.message);
  } finally {
    tmpFile.removeCallback();
    dockerfileTmp.removeCallback();
    dir.removeCallback();
  }
}

gcc有点区别,多了一个编译的步骤

async execGcc(execCode: ExecCode) {
  const dir = tmp.dirSync({});
  const tmpFile = tmp.fileSync({
    dir: dir.name,
    name: 'main.' + execCode.codeType,
  });
  fs.writeFileSync(tmpFile.name, execCode.sourceCode);

  const dockerfileContent = `
    FROM gcc
    COPY ${path.basename(tmpFile.name)} /app/
    WORKDIR /app
  `;
  const dockerfileTmp = tmp.fileSync({
    dir: dir.name,
    name: 'Dockerfile',
  });
  fs.writeFileSync(dockerfileTmp.name, dockerfileContent);
  if (
    !(await this.checkImage(CoderRunnerConstant.GCC_RUNNER_IMAGE_NAME))
  ) {
    await this.createImage(
      CoderRunnerConstant.GCC_RUNNER_IMAGE_NAME,
      dockerfileTmp,
      tmpFile,
    );
  }
  try {
    let container: Docker.Container | null = await this.checkContainer(CoderRunnerConstant.GCC_CONTAINER_NAME)
    if (!container) {
      container = await this.createContainer(CoderRunnerConstant.GCC_RUNNER_IMAGE_NAME, CoderRunnerConstant.GCC_CONTAINER_NAME)
    }
    await this.checkStatusAndStart(container)
    await this.copyFileContent(container, tmpFile);
    await this.gccCompile(container, tmpFile)
    const exec = await container.exec({
      Cmd: ['./main'],
      AttachStdin: true,
      AttachStdout: true,
      AttachStderr: true,
    });
    exec.start({ hijack: true, stdin: true }, function (err, stream) {
      if (err) return console.error(err);
      stream?.write(execCode.input);
      stream?.end();
      const stdout = new PassThrough();
      const stderr = new PassThrough();
      exec.modem.demuxStream(stream, stdout, stderr);
      stdout?.on('data', (chunk) => {
        console.log(chunk.toString())
      });
      stderr?.on('data', (chunk) => {
        console.log(chunk.toString())
      });

      stream?.on('end', async function () {
        console.log('执行完成')
      });
    });
  } catch (error) {
    console.log(error.message);
  } finally {
    tmpFile.removeCallback();
    dockerfileTmp.removeCallback();
    dir.removeCallback();
  }
}

编译gcc函数实现

async gccCompile(container: Container, tmpFile: tmp.FileResult) {
  const makeExec = await container.exec({
    Cmd: ['g++', `./${path.basename(tmpFile.name)}`, '-o', './main'],
    AttachStdout: true,
    AttachStderr: true
  })
  let output = ''
  const back = await makeExec.start({
  })
  // 必须等待编译完成,否则会导致误差
  return new Promise((resolve, reject) => {
    back.on('end', (e: any) => {
      if (output === '') {
        resolve(e)
      } else {
        reject(new Error(output))
      }
    })
    back.on('error', (err: any) => {
      console.error('Error during command execution:', err);
      reject(err);
    });
    back.on('data', (e: any) => {
      output += e.toString()
    })
  })
}

基于docker容器执行代码就完成了,包括java、cpp、python、如果要添加其他语言,也只需要下载对应镜像,改造一下运行方法即可,理论上支持所有语言。

成果中的在线代码编译器基于websocket的方式实现了页面与服务端的互动,前端通过client.emit({})发送过来一个execCode对象,后端接受到执行execCode方法,根据语言类型以适合的容器运行,运行结束将输出和错误信息均返回给前端。

只需实现websocket互通,就可以达成和文章首网站中一样的成果。

总结:nestjs框架有点东西,dockerode效率嘎嘎快,python、gcc、python瞬间执行完毕(不知道基于process进程的执行方式效率如何),good👍

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你熬夜了吗?

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值