当前位置: 首页 > article >正文

JAVA - OJ沙箱(JAVA默认模板沙箱,JAVA操作dokcer沙箱)

一 沙箱

        沙箱(Sandbox):一种安全机制,用于隔离运行环境,限制程序或代码的权限,防止其对系统或其他程序造成损害。沙箱通常用于运行不受信任的代码(如用户提交的代码、第三方插件等),以确保系统的安全性和稳定性。

        核心目标:

        隔离运行环境:为代码提供一个独立的运行环境,与主系统和其他程序隔离。从而确保,即使用户提供的代码有错误,或者崩溃,也不会影响到核心系统的正常运行。

        限制权限:沙箱通过权限控制机制,从而限制代码对系统资源的调用以及影响。

        安全性:通过权限限制以及对于运行环境的隔离,从而确保沙箱在可控的运行环境下运行,避免了沙箱对外部造成危害(例如删除资源,文件等)。

        沙箱的关键特点

  • 独立性:沙箱中的代码运行在一个独立的环境中,与外部系统隔离。

  • 权限控制:沙箱限制了代码的权限,确保其只能访问允许的资源。

  • 安全性:防止恶意代码对系统造成破坏。

  • 可恢复性:如果代码运行失败,沙箱可以快速清理环境并重新启动

二 JAVA默认沙箱

        这里我们先以JAVA默认沙箱为例,主要是依靠本地的JAVA环境运行,测试代码。

        实际上对于这两种代码沙箱的方式,大致都可以分为5个步骤:

        1. 保存用户代码,创建为文件

        2. 对用户代码文件进行编译

        3. 运行用户代码,并获取结果

        4. 收集整理输出结果

        5. 清理用户文件

        2.1 保存用户代码

        先梳理一下思路:

        既然要保存文件,就必须确定文件保存的路径,一种是直接保存在当前目录,一种是自定义固定文件保存路径。这里我选择直接保存在当前工作目录,也就是程序运行的目录。

        通过System.getProperty("user.dir")获取,如下。

        同时,单独创建一个保存文件的文件夹,用以保存所有代码。

        GLOBAL_CODE_DIR_NAME自定义即可。

        //返还当前用户工作目录的绝对路径  其实也就是我们代码存放的位置
        String userDir = System.getProperty("user.dir");
        String globalCodePathName = userDir + File.separator + GLOBAL_CODE_DIR_NAME;
        // 判断全局代码目录是否存在,没有则新建
        //将对应用户的所有代码都存放在一个目录当中 没有就创建
        if (!FileUtil.exist(globalCodePathName)) {
            FileUtil.mkdir(globalCodePathName);
        }

        再保存用户代码到对应文件当中,同时使用UUID拼接,最后加上Main.java,确保程序正确·命名,从而将用户代码隔离存放,并且返还保存的文件对象:

        // 把用户的代码隔离存放
        String userCodeParentPath = globalCodePathName + File.separator + UUID.randomUUID();
        //用户提交代码相关信息的文件名
        String userCodePath = userCodeParentPath + File.separator + GLOBAL_JAVA_CLASS_NAME;
        //写入 有三个参数:写入的字符串,保存的路径,对应保存文件的字符编码
        //代替保存的文件对象
        File userCodeFile = FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8);
        return userCodeFile;

        2.2代码文件编译

        文件保存后,使用命令进行编译,并且指定编译的字符集跟文件名,这里的userCodeFile就是上文保存的文件信息,getAbsolutePath()以获取绝对路径。

String compileCmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsolutePath());

        指令已经设定成功,那么如何使用JAVA执行外部操作?

        这里提供有两种方式,一种是ProcessBuilder

        另外一种是Runtime.getRuntime()

        Runtime.getRuntime()

        先说一下 Runtime.getRuntime(),这是一种比较老的方法了,将上面已经设置好的命令放入其中,即可执行对应的cmd命令。

        优点:操作比较简单,exec 方法接受一个字符串(String)作为参数。

        缺点:如果命令中包含空格或特殊字符,可能会导致参数解析错误。同时,无法支持链式调用,无法配置运行环境,代码发生异常时,提示的异常信息也比较少。

Process compileProcess = Runtime.getRuntime().exec(compileCmd);

        ProcessBuilder

        是一种比较现代的方法,整体比较全面,基本上可以满足各种用户的需求

        输入参数时,其采用列表的形式,避免了输入特殊字符造成的错误;同时还支持链式输入,配置文件目录,运行环境等。但是ProcessBuilder给出的方法比较多,学习成本相对比较高,并且对于我们当前的简单命令,也不需要配置什么环境,所以使用前者即可,有兴趣的可以了解一下ProcessBuilder

Process compileProcess = new ProcessBuilder("javac","-encoding","utf-8", userCodeFile.getAbsolutePath()).start();

        线程启动之后,我们获取到了Process,那么我们一起简单了解一下:

        Process类是 Java 中用于表示操作系统进程的一个类。它允许 Java 程序启动和管理外部进程(如执行系统命令、运行其他程序等),并与这些进程进行交互(如获取输入、输出、错误流,以及等待进程结束等)。

        获取到对应进程之后,我们就可以开始对此进程进行一个“监控”,例如编译是否正常,不正常的话编译错误信息是什么,编译时长是多少等。

        这里我们使用一个全新的概念(对我来说),StopWatch,作为一个计时器,相对于其他的计时器,其调用更加简单,高精度计时,甚至还能对多个程序同时计时,最后汇总操作时间等,有兴趣的搜索一下。

        计时开始之后,我们开始等待对应线程执行完毕,要判断线程是否执行完毕以及线程的执行状态,可以用对应Process的方法,waitFor,使用这个方法的时候,程序会暂时阻塞起来,直到对应的runProcess完全执行完之后,之后返回对应操作状态,才继续之后的操作。

        根据对应的状态码,我们就能判断是否正确执行了,返还0,正常执行;1有错误信息。

        剩下的比较简单,不再赘述:

//收集数据        
        ExecuteMessage executeMessage = new ExecuteMessage();

        try {
            //相当于计时器,用来计算对应代码编译的时间长度
            StopWatch stopWatch = new StopWatch();
            //开始计时
            stopWatch.start();
            // waitFor会暂时阻塞起来,直到对应的runProcess完全执行完之后,之后返回对应操作状态,才继续之后的操作
            int exitValue = runProcess.waitFor();
            executeMessage.setExitValue(exitValue);
            // 正常退出
            if (exitValue == 0) {
                System.out.println(opName + "成功");
                // 分批获取进程的正常输出
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(runProcess.getInputStream()));
                List<String> outputStrList = new ArrayList<>();
                // 逐行读取
                String compileOutputLine;
                while ((compileOutputLine = bufferedReader.readLine()) != null) {
                    outputStrList.add(compileOutputLine);
                }
                executeMessage.setMessage(StringUtils.join(outputStrList, "\n"));
            } else {
                // 异常退出
                System.out.println(opName + "失败,错误码: " + exitValue);
                // 分批获取进程的正常输出
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(runProcess.getInputStream()));
                List<String> outputStrList = new ArrayList<>();
                // 逐行读取
                String compileOutputLine;
                while ((compileOutputLine = bufferedReader.readLine()) != null) {
                    outputStrList.add(compileOutputLine);
                }
                executeMessage.setMessage(StringUtils.join(outputStrList, "\n"));

                // 分批获取进程的错误输出
                BufferedReader errorBufferedReader = new BufferedReader(new InputStreamReader(runProcess.getErrorStream()));
                // 逐行读取
                List<String> errorOutputStrList = new ArrayList<>();
                // 逐行读取
                String errorCompileOutputLine;
                while ((errorCompileOutputLine = errorBufferedReader.readLine()) != null) {
                    errorOutputStrList.add(errorCompileOutputLine);
                }
                executeMessage.setErrorMessage(StringUtils.join(errorOutputStrList, "\n"));
            }
            stopWatch.stop();
            executeMessage.setTime(stopWatch.getLastTaskTimeMillis());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return executeMessage;

        2.3文件执行

        文件编译之后,执行的操作跟编译大差不差,设置指令,填写用户输入,执行线程。

        我是将对应的输入放在List集合当中了,多个输入的话一个一个遍历,这里的输入对应的其实就是测试案例的输入了。

        使用executeMessageList 将其全部保存起来,进行返还,以便之后使用。

        String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath();

        List<ExecuteMessage> executeMessageList = new ArrayList<>();
        for (String inputArgs : inputList) {
            //当前默认的判题,其实就是在本地跑的,而非是基于docker上装配跑的
//            String runCmd = String.format("java -Xmx256m -Dfile.encoding=UTF-8 -cp %s Main %s", userCodeParentPath, inputArgs);
            String runCmd = String.format("java -Xmx256m -Dfile.encoding=UTF-8 -cp %s Main %s", userCodeParentPath, inputArgs);
            try {
                Process runProcess = Runtime.getRuntime().exec(runCmd);
                // 超时控制
                new Thread(() -> {
                    try {
                        Thread.sleep(TIME_OUT);
                        System.out.println("超时了,中断");
                        runProcess.destroy();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }).start();
                ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(runProcess, "运行");
                System.out.println(executeMessage);
                executeMessageList.add(executeMessage);
            } catch (Exception e) {
                throw new RuntimeException("执行错误", e);
            }
        }
        return executeMessageList;

        这里需要解释的应该就是对应的超时控制了,对应的线程启动之后,我们再创建一个新的线程,并且启动,这个新的线程是为了监控runProcess的,为了放置其运行时间过长而占用大量系统资源。如果在TIME_OUT,也就是我们规定的时间没有完成任务,那么就会销毁线程,释放资源。

        ProcessUtils.runProcessAndGetMessage(runProcess, "运行")跟编译时,设置对应编译时间,编译情况用的方法一样,这里用的是对运行时程序的信息填充。

        2.4 获取执行结果

        因为对应的算法题目一般都设置的有对应的时间限制,内存限制,所以我们需要将程序运行的结果再与题目要求的比对一下,设置运行程序的最大运行时间,以及占用内存等(获取占用内存比较繁琐)。不过多阐述。

        ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
        List<String> outputList = new ArrayList<>();
        // 取用时最大值,便于判断是否超时
        long maxTime = 0;
        for (ExecuteMessage executeMessage : executeMessageList) {
            String errorMessage = executeMessage.getErrorMessage();
            if (StrUtil.isNotBlank(errorMessage)) {
                executeCodeResponse.setMessage(errorMessage);
                // 用户提交的代码执行中存在错误
                executeCodeResponse.setStatus(3);
                break;
            }
            outputList.add(executeMessage.getMessage());
            Long time = executeMessage.getTime();
            if (time != null) {
                maxTime = Math.max(maxTime, time);
            }
        }
        // 正常运行完成
        if (outputList.size() == executeMessageList.size()) {
            executeCodeResponse.setStatus(1);
        }
        executeCodeResponse.setOutputList(outputList);
        JudgeInfo judgeInfo = new JudgeInfo();
        judgeInfo.setTime(maxTime);
        // 要借助第三方库来获取内存占用,非常麻烦,此处不做实现  第三方技术获取内存占用情况
        //judgeInfo.setMemory();
        executeCodeResponse.setJudgeInfo(judgeInfo);
        return executeCodeResponse;

        2.5删除用户代码

        程序既然已经运行完毕,那么自然是要将对应的文件进行删除,如下:

    public boolean deleteFile(File userCodeFile) {
        if (userCodeFile.getParentFile() != null) {
            String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath();
            boolean del = FileUtil.del(userCodeParentPath);
            System.out.println("删除" + (del ? "成功" : "失败"));
            return del;
        }
        return true;
    }

docker沙箱放到明天再更新,马上整理完毕   ^-^ ,希望对大家有所帮助。

这里对应用户,题目提交的服务是与沙箱代码分开的,这里着重介绍了沙箱。需要的话我也会更新,希望对大家有所帮助。


http://www.kler.cn/a/596760.html

相关文章:

  • MacOS安装 nextcloud 的 Virtual File System
  • LangChain组件Tools/Toolkits详解(6)——特殊类型注解Annotations
  • llama源码学习·model.py[4]Attention注意力(2)源码分析
  • 洛谷 [语言月赛 202503] 题解(C++)
  • (滑动窗口)算法训练篇11--力扣3.无重复字符的最长字串(难度中等)
  • ROM(只读存储器) 、SRAM(静态随机存储器) 和 Flash(闪存) 的详细解析
  • Centos编译升级libcurl
  • DeepSeek自学手册:《从理论(模型训练)到实践(模型应用)》|73页|附PPT下载方法
  • NVM 多版本node.js管理工具
  • Linux用户管理实操指南
  • 【 <二> 丹方改良:Spring 时代的 JavaWeb】之 Spring Boot 中的异常处理:全局异常与自定义异常
  • Ubuntu 系统安装 Redis 的详细步骤
  • Android13音频子系统分析(四)---座舱的多音区框架
  • 亮相AWE2025,MOVA以科技重塑生活,以美学沟通世界
  • go:前后端分离
  • Agent Team 多智能体系统解析
  • 【redis】事务详解,相关命令multi、exec、discard 与 watch 的原理
  • 嵌入式系统的核心组成部分处理器、存储器、传感器和执行器
  • 正则表达式详解(regular expression)
  • 掌握 Zapier:从入门到精通的自动化指南